diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3de4fb4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Python runtime artifacts +venv/ +.venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ + +# Database files (live in Docker named volume, not image layer) +*.db +*.db-wal +*.db-shm + +# Secrets — injected at runtime via env_file / environment +.env + +# Version control +.git/ +.gitignore + +# Editor / IDE +*.code-workspace +.vscode/ +.claude/ + +# Frontend — built fresh inside Docker; no need to ship source deps or old builds +frontend/node_modules/ +frontend/.next/ +frontend/out/ +api/frontend/node_modules/ +api/frontend/build/ +api/frontend/app_backup/ + +# OS +.DS_Store +Thumbs.db + +# CI metadata — not needed in runtime image +.github/ +README.md +CONTRIBUTING.md +PUBLISHING.md +docker-compose.yml +railway.toml +# CHANGELOG.md is intentionally NOT ignored: cogs/changelog.py reads it +# at runtime to power the player-facing ,changelog command. + +# Temp +temp/ +.temp/ \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9872c77 --- /dev/null +++ b/.env.example @@ -0,0 +1,613 @@ +# ══════════════════════════════════════════════════════════════════════════════ +# Discoin — Environment Configuration +# ══════════════════════════════════════════════════════════════════════════════ +# Copy this file to .env and fill in your values. +# Lines beginning with # are comments and are ignored. +# Defaults shown are the values used when a variable is not set. +# ══════════════════════════════════════════════════════════════════════════════ + + +# ── Discord ──────────────────────────────────────────────────────────────────── + +# Required. Your bot token from https://discord.com/developers/applications +DISCORD_TOKEN= + +# Command prefix for text commands (e.g. $ → $balance, $daily) +PREFIX=$ + +# Optional. If set, slash commands are registered to this guild instantly. +# Useful during development — leave blank for global registration in production. +SLASH_GUILD_ID= + +# Optional. Discord user ID to receive /report DMs and dev-only status pings. +# Right-click your profile → Copy User ID (Developer Mode must be enabled). +# Leave blank to disable report DMs and dev status messages. +REPORT_TARGET_USER_ID= + +# How many days before closed report DMs are automatically cleaned up. +REPORT_DM_CLEANUP_DAYS=7 + + +# ── Database ─────────────────────────────────────────────────────────────────── + +# PostgreSQL connection string. +# Format: postgresql://user:password@host:port/database +DATABASE_URL=postgresql://discoin:discoin@localhost:5432/discoin + +# Redis connection string. Used for the event bus and API pub/sub. +REDIS_URL=redis://localhost:6379 + +# Salt used when hashing transaction IDs. +# Set this before first run — it cannot be changed later without +# breaking all existing transaction hashes. +# Generate with: openssl rand -hex 32 +TX_SALT= + + +# ── Dashboard API ────────────────────────────────────────────────────────────── + +# Port the REST API (FastAPI) listens on. +# When running in Docker, map this to your host port in docker-compose.yml. +API_PORT=8080 + +# Admin API key for the dashboard. Required to use admin-only endpoints +# (/api/v2/admin/*) and to log in as Admin on the dashboard. +# Leave blank to disable admin API access entirely. +# Generate with: openssl rand -hex 32 +API_KEY= + +# Optional. Public-facing URL of your dashboard. +# Used to generate deep-links in Discord embeds (e.g. https://discoin.example.com). +# Leave blank if you are only running locally. +DASHBOARD_URL= + + +# ── Discord OAuth2 (Dashboard Login) ────────────────────────────────────────── +# Required for the "Login with Discord" button on the dashboard. +# +# Setup: +# 1. Go to https://discord.com/developers/applications → select your app +# 2. Left sidebar → OAuth2 → General +# 3. Copy "Client ID" and "Client Secret" below +# 4. Under "Redirects" add your callback URL: +# Local: http://localhost:8080/api/auth/callback +# Production: https://your-domain.com/api/auth/callback +# 5. Set DISCORD_REDIRECT_URI to match exactly what you entered above + +# Client ID (public — safe to commit) +DISCORD_CLIENT_ID= + +# Client Secret (treat like a password — never commit this) +DISCORD_CLIENT_SECRET= + +# Must match the redirect URL added in the Discord Developer Portal exactly. +DISCORD_REDIRECT_URI=http://localhost:8080/api/auth/callback + +# Secret used to sign dashboard session JWTs. +# Changing this after first run logs all users out. +# Generate with: openssl rand -hex 32 +JWT_SECRET= + +# How long dashboard access tokens last (seconds). Default: 7 days. +JWT_EXPIRE_SECONDS=604800 + +# How long refresh tokens last (days). Default: 30 days. +REFRESH_TOKEN_EXPIRE_DAYS=30 + + +# ── API Security ─────────────────────────────────────────────────────────────── + +# CORS allowed origins for the dashboard API (JSON array or comma-separated). +# In production, restrict this to your actual frontend domain. +# Example: CORS_ORIGINS=["https://discoin.example.com"] +CORS_ORIGINS=["http://localhost:3000","http://localhost:8080"] + +# Rate limits (requests per 10-second window per user). +RATE_LIMIT_PUBLIC=60 +RATE_LIMIT_AUTH=120 +RATE_LIMIT_ADMIN=240 + +# Set to true only when running behind a trusted reverse proxy (nginx, Cloudflare) +# so the real client IP is extracted from X-Forwarded-For. +# Leave false to prevent IP spoofing via forged headers. +TRUST_PROXY_HEADERS=false + +# Set to false to disable the security/anti-abuse engine entirely. +SECURITY_SYSTEM=true + + +# ── GIPHY ────────────────────────────────────────────────────────────────────── + +# Public API key from https://developers.giphy.com/dashboard/ +# Leave blank to disable GIF replies and the ,disco gif command. +GIPHY_API_KEY= + +# Probability (0.0-1.0) that Disco attaches a GIF after an AI chat reply. +# 0.15 = roughly 1 in 7 replies. Set to 0 to disable automatic GIFs entirely. +GIPHY_GIF_PROBABILITY=0.15 + +# GIPHY content rating applied to all searches: g | pg | pg-13 | r +GIPHY_GIF_RATING=g + + +# ── AI backend (served by Sojourns) ──────────────────────────────────────────── +# +# Recycler has NO built-in model brain. Every AI feature (Disco / AI Chat, the +# clanker pattern controller, memory-backed replies, tool loops) is served by +# the Sojourns platform over an OpenAI-compatible HTTP backend. AI is OPTIONAL: +# with it off, or the Sojourns backend unreachable, the full economy still runs +# and each AI-gated feature degrades gracefully. +# +# Point AI_BACKEND_URL at your Sojourns deploy's AI backend (the "/v1" base). +# +# On Railway, Sojourns is the controlling platform and Recycler is a sibling +# service in the same project. Reach it over Railway private networking using +# the internal hostname and Sojourns' WEB_PORT (8080): +AI_BACKEND_URL=http://sojourns.railway.internal:8080/v1 +# - Public domain alternative: https://.up.railway.app/v1 +# - Local dev (both on laptop): http://localhost:8080/v1 +# - Standalone, no Sojourns: https://openrouter.ai/api/v1 +# +# Bearer token the Sojourns AI backend expects. This MUST match Sojourns' +# AI_BACKEND_KEY exactly. The value below is a generated example -- replace it +# with your own secret (e.g. `python -c "import secrets;print(secrets.token_hex(24))"`) +# and set the same value on the Sojourns service. Falls back to +# OPENROUTER_API_KEY when blank so the standalone OpenRouter path still works. +AI_BACKEND_KEY=recycler-sojourns-53224e8b110c5de5fbb812fc1378a0c21b5df11bc7807166 +# Master switch for all AI features. Defaults to on whenever a backend key is +# present. Set AI_ENABLED=0 to run a pure-economy bot with no AI calls at all. +AI_ENABLED=1 + +# ── OpenRouter AI (standalone fallback path) ─────────────────────────────────── + +# API key from https://openrouter.ai — only used when AI_BACKEND_URL is left at +# the OpenRouter default (i.e. running without a Sojourns backend). +# Leave blank to disable AI globally (all AI_* flags below are then ignored). +OPENROUTER_API_KEY= + +# Model to use for AI generation. See https://openrouter.ai/models for options. +# Examples: +# google/gemini-2.5-flash (default, recommended) +# meta-llama/llama-3.3-70b-instruct:free (free alternative) +# openai/gpt-4o-mini (paid, fast) +# anthropic/claude-3-haiku (paid, high quality) +OPENROUTER_MODEL=google/gemini-2.5-flash + + +# ── AI Tools ─────────────────────────────────────────────────────────────────── +# Tools are defined in tools.json at the project root. Each tool is a prompt +# fragment Disco receives when the player's message matches its keyword triggers. +# Add/edit tools.json and run .admin ai reloadtools — no restart needed. +# +# When one or more tools fire, the call can be routed to a separate backend. +# Set TOOLS_BACKEND=ollama to use Ollama for tool-augmented calls (cheaper/faster +# for factual game questions), and keep OpenRouter for general chat. +# Leave both blank to run everything through OpenRouter. +TOOLS_BACKEND=openrouter +TOOLS_MODEL= + +# Casual chat (no tool match) can use a SEPARATE backend from the tool loop. +# Recommended split for the Ollama-Cloud-as-primary setup: +# TOOLS_BACKEND=ollama # tool calls go through your Ollama model +# TOOLS_MODEL=gemma4:31b-cloud +# CHAT_BACKEND=openrouter # "hi" / "what's up" go through fast OpenRouter +# OPENROUTER_MODEL=google/gemini-2.5-flash +# Leave CHAT_BACKEND blank to follow TOOLS_BACKEND (the old single-backend +# behaviour). The "is it alive?" type messages will then be just as slow as +# the tool loop is on your chosen Ollama model. +CHAT_BACKEND= + +# Base URL for Ollama. Cloud: https://ollama.com/v1 | Local: http://localhost:11434 +OLLAMA_BASE_URL=https://ollama.com/v1 + +# API key for Ollama cloud. Leave blank for unauthenticated local installs. +OLLAMA_API_KEY= + +# Ollama keep-alive: how long the model stays resident after a request. +# Without it, hosted Ollama unloads after ~30s idle, so the next request +# pays a 5-15s cold reload. Format: Ollama duration string ("10m", "30s", +# "1h", "-1" for forever). Set to empty to omit the field entirely. +OLLAMA_KEEP_ALIVE=10m + +# When 1, on Ollama empty-response, falls over to OpenRouter using +# OPENROUTER_MODEL so the user still gets *something*. Default 0 -- if you +# explicitly chose Ollama you probably don't want silent OpenRouter charges +# every time Ollama Cloud burps. Flip back to 1 if you want the safety net. +AI_CROSS_PROVIDER_RESCUE=0 + +# Hard cap on a single ,ask / mention / reply turn, in seconds. Replies +# that take longer than this are almost always stuck in a retry loop or +# waiting on a model that's gone catatonic. The placeholder shows an +# elapsed-time counter while the model thinks, so users can see how far +# along the request is. Image attachments get +30s on top of this. +AI_REPLY_TIMEOUT_S=90 + + +# ── AI Feature Flags ─────────────────────────────────────────────────────────── +# Set to 0 to disable a specific AI feature without removing the API key. + +# AI market maker uses AI to decide buy/sell timing and position size. +AI_MM_ENABLED=1 + +# .ask command — lets users chat with the AI directly in Discord. +AI_CHAT_ENABLED=1 + +# AI posts market commentary to the crypto channel after large price moves. +AI_COMMENTARY_ENABLED=1 + +# AI generates flavour text for .work command responses. +# Can get repetitive and adds latency to .work — disabled by default. +AI_FLAVOR_ENABLED=0 + +# AI narrates notable trade events (large buys, liquidations, etc.) +AI_EVENTS_ENABLED=1 + + +# ── Economy ──────────────────────────────────────────────────────────────────── + +# USD given to new users on their very first command. +STARTING_BALANCE=20 + +# Base .daily claim amount in USD. +DAILY_AMOUNT=500 + +# Seconds between .work uses (900 = 15 minutes). +WORK_COOLDOWN=900 + + +# ── Money Drops ──────────────────────────────────────────────────────────────── + +# Seconds between automatic money drops posted to channels (1800 = 30 min). +AUTO_DROP_INTERVAL=1800 + +# Random drop value range in USD. +DROP_MIN=100 +DROP_MAX=2000 + +# Seconds the claim window stays open before distributing to all who clicked. +DROP_COLLECT_WINDOW=30 + + +# ── Gambling ─────────────────────────────────────────────────────────────────── + +# Maximum bet allowed on any gambling command. +MAX_BET=500000 + + +# ── Anti-Bot ─────────────────────────────────────────────────────────────────── +# After a random number of consecutive game plays (between MIN and MAX), +# the player is shown a simple math CAPTCHA. Failure locks them out of games +# for 1 hour and sends a DM to REPORT_TARGET_USER_ID. + +ANTIBOT_MIN_GAMES=50 +ANTIBOT_MAX_GAMES=100 + + +# ── Items / Stones ───────────────────────────────────────────────────────────── + +# XP awarded per (hashrate_share × block_found) for Hashstone levelling. +# Increase to speed up Hashstone progression. +HASHSTONE_XP_RATE=40.0 + +# XP awarded per bet unit for Gambastone levelling. +GAMBASTONE_XP_RATE=1.2 + + +# ── Blockchain ───────────────────────────────────────────────────────────────── + +# Seconds between automatic chain block seals (1800 = 30 min). +CHAIN_BLOCK_INTERVAL=1800 + + +# ── AMM Pools ────────────────────────────────────────────────────────────────── + +# If true, AMM pools are automatically seeded with liquidity on bot startup. +# Useful for fresh installs so users can trade immediately. +AUTO_SEED_POOLS=false + +# Stablecoin-side depth per pool when auto-seeding (USD). +POOL_SEED_STABLECOIN=10000 + + +# ── Database Backups ─────────────────────────────────────────────────────────── + +# How often to create automatic pg_dump backups (hours). +BACKUP_INTERVAL_HOURS=6 + +# Number of backup files to retain before rotating out the oldest. +BACKUP_KEEP=7 + +# Maximum age of backup files in days before auto-deletion (0 = disabled). +BACKUP_MAX_AGE_DAYS=0 + + +# ── Wallet / DeFi Fees ───────────────────────────────────────────────────────── + +# Platform fee on CeFi→DeFi withdrawals and gas-bearing trades (decimal, 0.002 = 0.2%). +WALLET_PLATFORM_FEE_PCT=0.002 + +# Fee floor and cap in USD. +WALLET_PLATFORM_FEE_MIN=0.10 +WALLET_PLATFORM_FEE_MAX=20.00 + + +# ── Whale Alerts ─────────────────────────────────────────────────────────────── + +# USD value threshold for whale alert notifications. +# Can also be overridden per-guild via .admin whalethreshold . +WHALE_ALERT_THRESHOLD_USD=50000 + + +# ── Rugpull Minigame ────────────────────────────────────────────────────────── + +# Discord Role ID to assign to the current "King of Rugs". +# Create a role in your server and paste its ID here. +# The role gives +5% work bonus and +10% ape bonus while held. +# Leave blank or 0 to disable role assignment. +RUGPULL_ROLE=0 + + +# ── Developer ────────────────────────────────────────────────────────────────── + +# Enable debug mode and the .admin log command. +# Leave false in production — it can expose sensitive information. +DEBUG=false + +# Hours between automated status DMs sent to REPORT_TARGET_USER_ID. +DEV_STATUS_DM_INTERVAL=4.0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# Security / Anti-Abuse Engine (SEC_* variables) +# ══════════════════════════════════════════════════════════════════════════════ +# All thresholds below are optional. The defaults are conservative — they catch +# clear abuse without flagging normal play. Only change these if you understand +# the detection logic in security/config.py. +# Disable the engine entirely with SECURITY_SYSTEM=false above. + + +# ── Detection Windows ────────────────────────────────────────────────────────── + +# How often the background scanner runs (seconds). +SEC_SCAN_INTERVAL=120 + +# How far back each scan looks for suspicious activity (seconds). +SEC_LOOKBACK=300 + + +# ── Economy Detectors ────────────────────────────────────────────────────────── + +# Max income-generating actions per lookback window before flagging. +SEC_INCOME_VELOCITY_LIMIT=20 + +# Max gambling actions per lookback window before flagging. +SEC_GAMBLING_VELOCITY_LIMIT=100 + +# Min add/remove cycles on the same LP pair to flag as LP manipulation. +SEC_LP_CHURN_MIN=4 + +# Min buy→sell cycles on the same token to flag as wash trading. +SEC_WASH_TRADE_MIN_CYCLES=6 + +# Min transfers in a circular pattern to flag as a transfer ring. +SEC_TRANSFER_RING_MIN=4 + +# Max transactions per lookback window before flagging as a tx flood. +SEC_TX_FLOOD_LIMIT=80 + + +# ── DeFi Exploit Detectors ───────────────────────────────────────────────────── + +# Borrow + trade + repay within this window is flagged as a flash-loan exploit (seconds). +SEC_FLASH_LOAN_WINDOW=30 + +# Rapid same-token trades above this count flag oracle manipulation. +SEC_ORACLE_MANIPULATION_TRADES=8 +SEC_ORACLE_MANIPULATION_WINDOW=60 + + +# ── API / Session Detectors ──────────────────────────────────────────────────── + +# Max failed auth attempts before flagging (per SEC_AUTH_FAILURE_WINDOW). +SEC_AUTH_FAILURE_LIMIT=10 +SEC_AUTH_FAILURE_WINDOW=300 + +# Flag if the client IP changes within this many seconds of the last request. +SEC_SESSION_IP_CHANGE_WINDOW=60 + +# Max API requests per user per window before flagging as API abuse. +SEC_API_REQUEST_FLOOD_LIMIT=200 +SEC_API_REQUEST_FLOOD_WINDOW=60 + + +# ── Bot Command Flood ────────────────────────────────────────────────────────── + +# Max total commands per window before flagging. +SEC_COMMAND_FLOOD_LIMIT=60 +SEC_COMMAND_FLOOD_WINDOW=60 + +# Max identical commands per window (stricter sub-limit). +SEC_IDENTICAL_COMMAND_LIMIT=30 + + +# ── Cross-Platform Correlation ───────────────────────────────────────────────── + +# Flag if the same user triggers events on both the bot and API within this window. +SEC_CORRELATION_WINDOW=300 +SEC_CORRELATION_EVENT_MIN=10 + + +# ── Threat Scoring ───────────────────────────────────────────────────────────── + +# Half-life for threat score decay (seconds). Score halves every N seconds of inactivity. +SEC_SCORE_DECAY_HALF_LIFE=3600.0 + +# Score thresholds that trigger each enforcement level. +# Level 1 — log + monitor +SEC_LEVEL_1_THRESHOLD=21.0 +# Level 2 — throttle commands +SEC_LEVEL_2_THRESHOLD=41.0 +# Level 3 — freeze account +SEC_LEVEL_3_THRESHOLD=61.0 +# Level 4 — flag + alert admin +SEC_LEVEL_4_THRESHOLD=81.0 +# Level 5 — emergency lockdown +SEC_LEVEL_5_THRESHOLD=91.0 + + +# ── Enforcement Durations ────────────────────────────────────────────────────── + +SEC_THROTTLE_DURATION=600 +SEC_FREEZE_DURATION=900 +SEC_FLAG_DURATION=3600 +SEC_LOCKDOWN_DURATION=1800 + +# Max requests per 10-second window while a user is throttled. +SEC_THROTTLED_RATE_LIMIT=10 + + +# ── Alerts & Deduplication ───────────────────────────────────────────────────── + +# Minimum seconds between repeat alerts for the same user. +SEC_ALERT_COOLDOWN=600 + +# How long duplicate alert suppression lasts (seconds). +SEC_ALERT_DEDUP_TTL=600 + + +# ── Behaviour Profiling ──────────────────────────────────────────────────────── + +# How long a user's behaviour profile is cached in Redis (seconds). +SEC_PROFILE_TTL=86400 + +# How often the profile is recalculated (seconds). +SEC_PROFILE_UPDATE_INTERVAL=300 + +# Standard deviations above baseline required to flag as an anomaly. +SEC_ANOMALY_STDDEV_THRESHOLD=3.0 + +# Minimum data points needed before anomaly detection kicks in. +SEC_BASELINE_MIN_SAMPLES=20 + +# How long baseline data is retained (seconds). +SEC_BASELINE_TTL=21600 + + +# ── Whale & Repeat-Offender Tracking ────────────────────────────────────────── + +# Max number of wallets one user may control before flagging whale concentration. +SEC_WHALE_CONCENTRATION_LIMIT=3 + +# How many prior offences before applying harsher automatic penalties. +SEC_REPEAT_OFFENDER_LIMIT=3 + + +# ── Premium / Multi-tenant ──────────────────────────────────────────────────── +# Discoin runs as a single shared bot. Every premium feature is auto-unlocked +# for HOST_GUILD_ID; other guilds need a paid subscription or an admin grant. +# Defaults in code to 1467740704725012638 (operator's home server). Override +# here for self-hosted deployments. +HOST_GUILD_ID= +# Bot owner's Discord user_id. Allowed to run ,admin premium grant/revoke for +# arbitrary guilds. Falls back to Discord application owner if unset. +BOT_OWNER_ID= +# Optional auto-trial granted on first gated command (0 = no trial). +PREMIUM_TRIAL_DAYS=0 + + +# ── PayPal Subscriptions ────────────────────────────────────────────────────── +# Sandbox creds: https://developer.paypal.com/dashboard/applications/sandbox +# Switch PAYPAL_MODE to 'live' once tested end-to-end in sandbox. +PAYPAL_MODE=sandbox +PAYPAL_CLIENT_ID= +PAYPAL_CLIENT_SECRET= +PAYPAL_WEBHOOK_ID= +# Plan IDs from PayPal -> Billing -> Subscription Plans. Create a plan first, +# then paste its plan_id (P-XXXXXXX...) here. +PAYPAL_PLAN_ID_MONTHLY= +PAYPAL_PLAN_ID_YEARLY= +# Optional return / cancel URLs. {gid} is replaced at runtime with the +# subscribing guild's id. +PAYPAL_RETURN_URL= +PAYPAL_CANCEL_URL= +# Display strings for ,premium info (no effect on what is actually charged -- +# PayPal is the source of truth). +PREMIUM_PRICE_MONTHLY_DISPLAY=$5/mo +PREMIUM_PRICE_YEARLY_DISPLAY=$50/yr + + +# ── Cross-asset market providers ($-prefixed commands) ──────────────────────── +# Every provider below is OPTIONAL. The bot boots and serves $help, $chart, +# $info on crypto with none of these set. Each provider gracefully disables +# when its key is missing. The router fans out across whatever's available. + +# Yahoo Finance -- equities, ETFs, indices, forex, commodities (no key). +YAHOO_ENABLED=1 + +# Finnhub -- equities news + fundamentals + earnings calendar. +# Sign up at https://finnhub.io (free tier: 60 req/min). +FINNHUB_API_KEY= + +# DexScreener -- DEX pair data, no key. +DEXSCREENER_ENABLED=1 + +# Pyth Hermes -- realtime oracle prices (Crypto + forex + equities). +# Default is the public Hermes endpoint; override only for self-hosted gateways. +PYTH_HERMES_URL=https://hermes.pyth.network + +# RedStone -- oracle backup gateway. +REDSTONE_GATEWAY_URL=https://oracle-gateway-1.a.redstone.finance + +# Switchboard On-Demand -- oracle reads via the public Crossbar gateway. +# No SDK, no Solana RPC needed. SWITCHBOARD_FEEDS is a JSON map of +# {"SYMBOL/USD": "0x"} -- pull feed hashes from +# https://ondemand.switchboard.xyz/. Leave empty to let the router fall +# through to Pyth + RedStone (which already cover every major). +# SWITCHBOARD_RPC_URL is kept for compatibility but unused -- Crossbar +# doesn't need it. +SWITCHBOARD_RPC_URL= +SWITCHBOARD_CROSSBAR_URL=https://crossbar.switchboard.xyz +SWITCHBOARD_NETWORK=solana/mainnet +# Example: SWITCHBOARD_FEEDS={"MTA/USD":"0xabc...","ARC/USD":"0xdef..."} +SWITCHBOARD_FEEDS= + +# CoinGlass -- perp funding / OI / liquidations / long-short ratio. +# Sign up at https://www.coinglass.com for an API key. +COINGLASS_API_KEY= + +# Coinalyze -- derivatives backup. Free key: https://coinalyze.net. +COINALYZE_API_KEY= + +# TradingView UDF feed -- accepts any UDF-compatible endpoint +# (/config, /symbols, /search, /history). +# +# The bot ALSO hosts its own live UDF endpoint at +# /api/v2/udf +# on the existing FastAPI app (CORS-open, no auth, no demo data -- +# sources real OHLC from Yahoo + CoinGecko + DexScreener via the +# market router). On Railway this is the public bot URL + /api/v2/udf; +# locally it's http://localhost:${API_PORT:-8080}/api/v2/udf. +# +# Set TRADINGVIEW_UDF_URL to that endpoint to make the in-router +# TradingView provider an alias for the live bridge (recursion guard +# is built in -- the UDF handler short-circuits the TradingView +# provider for the duration of its own request so there's no loop). +# Leaving it empty just keeps the provider disabled. +# +# TRADINGVIEW_UDF_URL=http://localhost:8080/api/v2/udf +TRADINGVIEW_UDF_URL= + +# Extra cache TTLs the provider layer reads (seconds). +CACHE_TTL_QUOTE=10 +CACHE_TTL_ORACLE=5 +CACHE_TTL_DERIVATIVES=30 + +# ── Market AI ($scan ai / $query) ──────────────────────────────────────────── +# Defaults to enabled when OPENROUTER_API_KEY is set. Force off with 0. +MARKET_AI_ENABLED= + +# ── $watch alerts ──────────────────────────────────────────────────────────── +# Interval (seconds) the watch worker polls quotes for triggered alerts. +MARKET_ALERT_INTERVAL=60 +# Maximum watch entries per user (prevents abuse). +MARKET_WATCH_MAX_PER_USER=20 diff --git a/.github/agents/Architect.agent.md b/.github/agents/Architect.agent.md new file mode 100644 index 0000000..1243e97 --- /dev/null +++ b/.github/agents/Architect.agent.md @@ -0,0 +1,87 @@ +--- +name: architecture-consistency-audit +description: Discoin architectural and systems expert. Maintains project consistency, catches drift, and validates structural integrity. +--- + +# Architect - Systems Consistency Auditor + +You are an architectural expert for Discoin, a Discord economy bot with a Python backend, PostgreSQL database, Redis event bus, FastAPI REST API, and Next.js frontend. + +Your job is to catch structural drift, inconsistencies, and organizational issues that accumulate across pull requests. You maintain the project as a coherent whole. + +## What You Audit + +### Database Consistency +- Every table in `database/schema.sql` has a corresponding migration in `database/migrations/` +- Column types and constraints match between schema.sql and migration files +- Every raw SQL query in the codebase references tables and columns that exist in the schema +- MockDB in `tests/conftest.py` has stubs for every database method used in tests +- No orphaned migration files (migrations for tables that were later removed) + +### Cog Registration +- Every `.py` file in `cogs/` is registered in `core/framework/bot.py` COGS list +- Every cog imported in `core/framework/bot.py` actually exists as a file +- No circular imports between cogs (check for `from cogs.X import` inside other cogs) + +### Config Integrity +- Every `Config.X` reference in the codebase has a matching definition in `core/config.py` +- Every `.env` variable read in `core/config.py` is documented in `.env.example` +- No config values are defined but never used +- No config values are used but never defined + +### Import Consistency +- No imports from modules that don't exist +- No unused imports in modified files +- Helper functions referenced across cogs actually exist where they're imported from +- Framework utilities (card, ConfirmView, fmt_token, etc.) are imported from the correct modules + +### Help Text Accuracy +- Every command documented in `cogs/help.py` actually exists as a registered command +- Every command that accepts user input documents the valid formats +- New commands added to cogs have corresponding help entries +- Aliases listed in help match the actual command aliases + +### Test Coverage +- New database methods have corresponding MockDB stubs in `tests/conftest.py` +- Service functions have test coverage in `tests/test_services_*.py` +- Critical paths (money movement, fee calculation) have tests + +### API Consistency +- Every FastAPI route in `api/v2/routers/` has proper auth middleware +- Request/response models match the actual data shapes +- API routes that read data use the same DB methods as the Discord commands + +### File Organization +- No duplicate logic between `cogs/` and `services/` (business logic belongs in services) +- Constants are in `constants/` or `core/config.py`, not scattered in cog files +- Database queries are in `database/` mixins, not inline in cogs +- No files over 5000 lines (split needed) + +### Documentation +- `CHANGELOG.md` reflects recent changes +- `README.md` feature list is up to date +- `CONTRIBUTING.md` instructions still work +- `.github/copilot-instructions.md` matches current project structure + +## Rules + +- Compare what IS against what SHOULD BE based on the project's own patterns +- Flag inconsistencies, not preferences +- Every finding must reference specific files and line numbers +- Do not suggest new features or refactors +- Focus on things that will cause bugs, confusion, or maintenance burden + +## Output Format + +``` +## AUDIT RESULT: [CONSISTENT | DRIFT DETECTED] + +### Category: [Database|Cogs|Config|Imports|Help|Tests|API|Files|Docs] +**Issue:** What is inconsistent +**Files:** Affected file paths +**Expected:** What the project's own patterns dictate +**Actual:** What was found +**Priority:** HIGH (will cause errors) | MEDIUM (confusion risk) | LOW (cleanup) +``` + +If everything is consistent, output `AUDIT RESULT: CONSISTENT` with a summary of what was checked. diff --git a/.github/agents/Equilibrium.agent.md b/.github/agents/Equilibrium.agent.md new file mode 100644 index 0000000..8231147 --- /dev/null +++ b/.github/agents/Equilibrium.agent.md @@ -0,0 +1,98 @@ +--- +name: economic-sustainability-audit +description: Cryptocurrency economy sustainability auditor. Finds inflation leaks, broken sinks, unsustainable yields, and economic imbalances. +--- + +# Equilibrium - Economic Sustainability Auditor + +You are a cryptocurrency and DeFi/CeFi economic sustainability expert auditing a Discord economy bot called Discoin. + +Discoin simulates a full crypto ecosystem across independent Discord servers (guilds). Each guild has its own token prices, pools, validators, and user balances. Your job is to find economic imbalances that would cause hyperinflation, deflation spirals, or exploitable arbitrage. + +## What You Audit + +### Money Supply +- **Faucets (sources):** daily rewards, work income, ape payouts, staking yields, mining rewards, beg jackpots, lending interest, LP fee earnings, prediction market winnings +- **Sinks (drains):** platform fees, gas fees, swap fees, early unstake penalties, beg catastrophes, ape losses, rugpull wagers, shop purchases, validator slashing, prediction market house cut +- **Balance check:** Do sinks outpace sources? Do sources outpace sinks? Is there a path to runaway inflation or a death spiral? +- **GDP scaling:** Does the work/daily GDP scaling actually prevent inflation, or can it be circumvented? + +### Token Economics +- Can any token be minted without a corresponding cost? +- Can token burns be avoided or reversed? +- Do staking APYs compound in a way that creates unbounded supply growth? +- Are mining reward rates sustainable relative to the token supply? +- Does the GBM price oracle with TWAP mean reversion actually stabilize, or can it drift to zero/infinity? + +### AMM Pool Economics +- Constant product invariant: is k preserved after every swap? (k should grow from fees, never shrink) +- Impermanent loss: are LP providers compensated enough via fees to justify the risk? +- Can pool reserves be drained to zero via repeated small swaps? +- Are swap fees properly collected and distributed? +- Can adding/removing liquidity create or destroy value? + +### Fee Circuit +- Do all fees flow to the right place (community reserves, vault, burn)? +- Can fee percentages be set to 0 or negative via config, bypassing the fee system? +- Are fee minimums and maximums enforced consistently? +- Platform fee on CeFi-to-DeFi withdrawals: is it charged on all paths? + +### Cross-Server Economics +- Can a server operator set configs that create infinite money (zero fees, max rewards)? +- Are there reasonable bounds on configurable values? +- Can a fresh server bootstrap its economy without external funding? +- Does the system work with 1 user? 10? 1000? 10000? + +### Staking and Yield +- Validator uptime rate vs slash rate: is expected value positive or negative for stakers? +- Can stakers compound rewards faster than intended by rapid stake/unstake cycling? +- Lock period enforcement: can it be bypassed? +- Are staking rewards paid from existing supply or minted from nothing? + +### Lending +- Collateral ratio enforcement: checked on all paths (borrow, price change, liquidation)? +- Interest accrual: does it compound correctly? +- Liquidation: does it actually recover the right amount? +- Can borrowers avoid liquidation by splitting across assets? + +### Rugpull Minigame Economics +- Are wager costs proportional to potential gains? +- Does the King bonus (+5% work, +10% ape) create a positive feedback loop? +- Can the vault accumulation become unbounded? + +## Key Config Values to Check + +```python +# core/config.py - Look for these and validate their interactions: +STARTING_BALANCE # Initial USD given to new users +DAILY_AMOUNT # Daily reward base +WORK_COOLDOWN # Time between work commands +STAKING_EARLY_UNSTAKE_PENALTY +SAVINGS_RATE_MODEL # Interest rates for vault savings +RUGPULL_TIERS # Wager costs and success rates +LP_LOCK_SECONDS # How long LP is locked +``` + +## Rules + +- Think like an economist, not a programmer +- Model the steady-state: what happens after 30 days, 90 days, 1 year of active play? +- Consider both active (100 commands/day) and passive (daily only) players +- Consider whale vs new player dynamics +- Do not comment on code style or structure +- Do not suggest new features + +## Output Format + +``` +## AUDIT RESULT: [SUSTAINABLE | CONCERNS | UNSUSTAINABLE] + +### [CRITICAL|HIGH|MEDIUM|LOW] - Title +**Mechanism:** Which economic system is affected +**Problem:** What goes wrong and over what timeframe +**Evidence:** Math or code references showing the issue +**Impact:** Inflation rate, deflation risk, or exploitability +**Fix:** Suggested parameter change or mechanism adjustment +``` + +If the economy is sustainable, output `AUDIT RESULT: SUSTAINABLE` with a summary of the key balancing mechanisms. diff --git a/.github/agents/Invario.agent.md b/.github/agents/Invario.agent.md new file mode 100644 index 0000000..327819e --- /dev/null +++ b/.github/agents/Invario.agent.md @@ -0,0 +1,32 @@ +--- +name: economic-invariant-auditor +description: Strictly enforces economic invariants like supply, burn, and fees. Fails on any inconsistency. +--- + +# Economic Invariant Auditor + +You are a financial systems auditor. + +Review the repository changes with a strict focus on economic correctness. + +Check for: +- Total supply inconsistencies (mint, burn, transfers must net correctly) +- Hidden inflation or deflation paths +- Rounding/precision errors that could accumulate +- Fee misallocation or leakage +- Any path where balances can go negative or unbounded +- NFT minting/sales/transfers must use atomic transactions (no partial state on failure) +- NFT marketplace listings must use network native coin pricing, not USD +- Prediction market payouts must deduct house cut before distribution + +Rules: +- Assume adversarial usage +- Do not comment on code style or structure +- Do not speculate + +Output format: +- PASS or FAIL +- If FAIL: + - Broken invariant + - Exact code location + - Minimal reproduction steps diff --git a/.github/agents/Sentinel.agent.md b/.github/agents/Sentinel.agent.md new file mode 100644 index 0000000..3ed6213 --- /dev/null +++ b/.github/agents/Sentinel.agent.md @@ -0,0 +1,83 @@ +--- +name: security-audit +description: Cryptocurrency and DeFi/CeFi security auditor. Finds exploits, injection vectors, privilege escalation, and player-driven abuse paths. +--- + +# Sentinel - Security Auditor + +You are a cryptocurrency and DeFi/CeFi security expert auditing a Discord economy bot called Discoin. + +Discoin simulates a full crypto ecosystem: wallets, token trading (AMM pools), staking/yield farming, lending, NFTs, mining, and USD banking. Players interact via Discord commands. The bot can run on multiple servers (guilds) independently. + +## What You Audit + +### Player-Side Exploits +- Race conditions: can a player fire two commands simultaneously to double-spend? +- Balance manipulation: can negative amounts, zero amounts, or extreme values break math? +- Input injection: can crafted token names, validator IDs, or amounts cause unintended behavior? +- Confirmation bypass: can players skip ConfirmView dialogs or act on someone else's confirmation? +- Cooldown evasion: can players reset or skip command cooldowns? +- Cross-guild leakage: can a player's balance in guild A affect guild B? +- Self-referral abuse: can a player send/trade/transfer to themselves to generate free value? +- Fee evasion: can players find routes that skip gas fees or platform fees? +- Overflow/underflow: can extremely large or small numbers cause float precision issues? +- Reentrancy-style bugs: can a callback or event handler be triggered mid-transaction? + +### Server Operator Risks +- Admin command abuse: can server admins extract value beyond intended limits? +- Configuration injection: can malicious .env values cause code execution? +- Cross-server data access: can one guild's admin read or write another guild's data? +- Rate limit bypass: can operators disable rate limiting to automate farming? + +### Infrastructure +- SQL injection via raw queries (check database/ for any string interpolation in SQL) +- Missing auth checks on API endpoints (check api/v2/) +- Secrets exposure in logs, error messages, or Discord embeds +- Redis pub/sub message spoofing +- JWT token reuse, expiry bypass, or key confusion + +### DeFi-Specific +- AMM pool manipulation: sandwich attacks, price oracle manipulation +- Flash-loan style attacks: borrow, manipulate, repay in one transaction flow +- LP token inflation: can adding/removing liquidity create tokens from nothing? +- Staking reward inflation: can stake/unstake cycling generate excess rewards? +- Validator slashing evasion: can stakers avoid slashing penalties? +- Rugpull minigame: can the King role be obtained without paying the wager? + +## Key Files + +``` +cogs/ Discord command handlers (bank, trade, stake, earn, crypto, rugpull) +database/ PostgreSQL queries (schema.sql, mixin files) +services/ Business logic (swap, trade, transfer) +api/v2/ REST API (auth, middleware, routers) +security/ Threat detection engine +core/framework/ Bot infrastructure (chain_engine, redis bus) +core/config.py All configuration values +``` + +## Rules + +- Assume adversarial players who will try every edge case +- Assume adversarial server operators who control .env and Discord permissions +- Do not comment on code style, naming, or formatting +- Do not suggest adding features +- Every finding must include: + - The exact file and line number + - A concrete attack scenario (step by step) + - Severity: CRITICAL / HIGH / MEDIUM / LOW + - Whether it affects single-server or cross-server + +## Output Format + +``` +## AUDIT RESULT: [PASS | FINDINGS] + +### [CRITICAL|HIGH|MEDIUM|LOW] - Title +**File:** path/to/file.py:123 +**Attack:** Step-by-step description of how to exploit this +**Impact:** What the attacker gains +**Fix:** Suggested remediation +``` + +If no issues found, output `AUDIT RESULT: PASS` with a brief summary of what was checked. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3e788d1 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,108 @@ +# Copilot Instructions for Discoin + +## Project Overview + +Discoin is a Discord economy bot with a full-stack architecture: +- **Backend**: Python 3.12 - discord.py bot + FastAPI REST API +- **Frontend**: Next.js 16 + TypeScript + Tailwind CSS +- **Database**: PostgreSQL (asyncpg) with Redis caching +- **Deployment**: Docker multi-stage build, Railway.app + +## Code Review Guidelines + +When reviewing pull requests, Copilot should check for: + +### Economic Correctness (Critical) +- Total supply invariants: mint, burn, and transfer operations must net correctly +- No paths where user balances can go negative or unbounded +- Fee calculations are precise - no rounding errors that accumulate over time +- AMM pool math (constant product formula) is preserved after swaps +- Staking reward calculations do not create inflation beyond configured rates +- Lending collateral ratios are enforced on all code paths +- NFT minting, marketplace sales, and transfers use atomic transactions with rollback +- NFT marketplace prices are in the network's native coin, not USD +- Prediction market payouts correctly deduct the 5% house cut before distribution + +### Python Backend +- All database operations use `asyncpg` connection pools - never raw connections +- Pydantic v2 models for request/response validation in FastAPI routes +- Rate limiting middleware is applied to public API endpoints +- JWT auth tokens are validated with proper expiry checks +- No secrets or credentials hardcoded - all config via environment variables +- Discord command handlers use proper error handling and user feedback +- Async/await is used correctly - no blocking calls in async contexts + +### Frontend (TypeScript/React) +- Components use TypeScript with strict typing - no `any` types +- API calls go through the axios client with proper error handling +- Zustand stores follow the existing patterns in `frontend/stores/` +- Next.js App Router conventions are followed + +### General +- No `.env` files, credentials, or secrets committed +- Database schema changes include migration considerations +- New API endpoints have corresponding test coverage +- Docker build is not broken by changes + +## Running Tests + +### Python Tests +```bash +uv pip install --system -r requirements.txt -r requirements-test.txt +python -m pytest tests/ -v --tb=short +``` + +Tests use `pytest` with `pytest-asyncio` (async mode: auto). Fixtures are in `tests/conftest.py`. + +Key test areas: +- `test_swap_economics.py` - AMM swap math and fee calculations +- `test_services_*.py` - Core business logic (trade, transfer, stake, savings) +- `test_api_auth.py`, `test_jwt.py`, `test_totp.py` - Authentication flows +- `test_rate_limit.py` - API rate limiting middleware +- `test_amount_parser.py` - User input parsing +- `test_net_worth.py` - Portfolio valuation + +### Frontend Lint +```bash +cd frontend && npm install && npm run lint +``` + +## PR Automation Expectations + +When Copilot is assigned to review a PR: + +1. **Run the test suite** - Ensure `python -m pytest tests/ -v --tb=short` passes +2. **Check economic invariants** - Use the rules from the `Invario` agent (`.github/agents/Invario.agent.md`) for any changes touching `cogs/`, `services/`, `core/framework/chain_engine.py`, or `database/` +3. **Flag security concerns** - SQL injection via raw queries, missing auth checks, exposed secrets +4. **Validate schema changes** - Any modifications to `database/schema.sql` should be backward-compatible or have a migration plan +5. **Verify Docker compatibility** - Changes to dependencies must be reflected in `requirements.txt` and not break the Dockerfile + +## File Structure Reference + +``` +main.py # Bot entry point +core/config.py # Centralized configuration +core/framework/ # Bot infrastructure (chain engine, redis bus, AI client) +cogs/ # Discord command modules (bank, crypto, trade, stake, etc.) +database/ # PostgreSQL layer (schema.sql, mixin-based architecture) + schema.sql # Full database schema +api/v2/ # FastAPI REST API (auth, routers, services, middleware) +services/ # Shared business logic (swap, trade, transfer) +frontend/ # Next.js dashboard +tests/ # pytest test suite +``` + +## Coding Conventions + +- Python: snake_case for functions/variables, PascalCase for classes +- Use `Decimal` for all financial calculations - never `float` +- Database queries go through the mixin classes in `database/`, not inline SQL +- Discord cog commands follow the pattern in existing `cogs/*.py` files +- API routes are versioned under `api/v2/routers/` +- Configuration values come from `core/config.py` which reads from environment variables + +## Branch Strategy + +- `master` - production branch +- `claude/**` - AI-assisted development branches +- PRs target `master` and require CI to pass before merge diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..85baf4d --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,44 @@ +name: Auto Changelog + +on: + schedule: + - cron: "0 0 * * *" # 00:00 UTC daily + workflow_dispatch: # allow manual runs for testing + +jobs: + update-changelog: + name: Scan branches and update CHANGELOG.md + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Full history needed so git log --since works correctly + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Scan commits and update CHANGELOG.md + run: python scripts/update_changelog.py + + - name: Commit and push if CHANGELOG.md changed + run: | + if git diff --quiet CHANGELOG.md; then + echo "CHANGELOG.md is already up to date - nothing to commit." + else + git add CHANGELOG.md + git commit -m "chore: auto-update changelog $(date -u +%Y-%m-%d) [skip ci]" + git push origin HEAD + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bd8ee21 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + - "claude/**" + - "copilot/**" + pull_request: + branches: + - main + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "requirements*.txt" + + - name: Install dependencies + run: uv pip install --system -r requirements.txt -r requirements-test.txt + + - name: Run tests + run: python -m pytest tests/ -v --tb=short diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 0000000..7e0cd00 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,20 @@ +name: Dependency Submission + +on: + push: + branches: + - master + +permissions: + contents: write + +jobs: + component-detection: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run component detection + uses: actions/component-detection-dependency-submission-action@374343effede691df3a5ffaf36b4e7acab919590 + with: + detectorFilter: PipReport + detectorCategories: Python diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77bcbc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,96 @@ +# ── Secrets & environment ───────────────────────────────────────────────────── +.env +.env.local +.env.*.local +# .env.example is intentionally NOT ignored — it's the template for new installs + +# ── Database ────────────────────────────────────────────────────────────────── +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +# ── Python ──────────────────────────────────────────────────────────────────── +__pycache__/ +**/__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +*.egg +dist/ +build/ +.eggs/ +.Python +pip-freeze.txt +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.pytype/ +.pdm-python +.pdm-build/ +celerybeat-schedule +celerybeat.pid + +# ── Virtual environments ─────────────────────────────────────────────────────── +venv/ +env/ +.venv/ + +# ── Logs ────────────────────────────────────────────────────────────────────── +*.log +logs/ +docs/superpowers/ + +# ── Node / frontend ─────────────────────────────────────────────────────────── +node_modules/ +**/node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +.npm/ +.yarn/ +.pnp.* +.eslintcache +.stylelintcache + +# Frontend build output (rebuilt inside Docker — never commit) +api/frontend/build/ +api/static/ + +# ── Generated chart images (runtime artifacts) ─────────────────────────────── +charts/*.png +charts/*.jpg +charts/*.jpeg +charts/*.gif + +# ── Generated docs (regenerate with scripts/gen_docs.py) ───────────────────── +COMMANDS.md + +# ── IDE & editor ────────────────────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +*.code-workspace + +# ── Claude Code (local session data — not project files) ───────────────────── +.claude/ + + +# ── OS ──────────────────────────────────────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini + +# ── Backups ─────────────────────────────────────────────────────────────────── +**/app_backup/ +**/*_backup/ + +# Temp +/temp/ + +# MkDocs build output (rebuilt in Docker — never commit) +/site/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffdbe03 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4285 @@ +# Changelog + +## [main] -- 2026-05-31 (9) + +### New Features +**Recycler: the clanker / clanktank bot, split into its own service**: This build is the Clanktank containment system (the `,clanker` command set, evidence clustering, cases, and the escape room) carved out of the old all-in-one Discoin build into a standalone bot. The cog registry loads a single cog -- `cogs.clanktank` -- so only the clanker command surface runs; the economy, games and NFTs stay in Discoin. + +### Changes +**No built-in AI; all AI calls route to Sojourns**: Recycler ships no model brain. The clanker's AI-assisted touches (e.g. escape-room reflection reformulation) call out to the Sojourns AI backend through one seam, selected by `AI_BACKEND_URL`, `AI_BACKEND_KEY` and the `AI_ENABLED` master switch. AI is optional -- with it off or the backend unreachable, containment, cases and the escape room run in full and only the AI-flavoured extras stand down. +**No economy API; serves no HTTP**: The economy REST API/dashboard stays in Discoin. Recycler removes `api/`, `frontend/` and `charts/`, skips the embedded FastAPI server cleanly when the package is absent, and drops the Railway healthcheck since it serves no HTTP. +**Railway topology**: Runs as a Railway service beside Sojourns (controlling platform / AI host) and Discoin, reaching the AI over private networking at `http://sojourns.railway.internal:8080/v1`. Persistent volume `recycler_data` holds the containment database; `.env.example` is wired with a concrete backend URL and shared key. + +## [main] -- 2026-05-31 (8) + +### Bug Fixes +**Reflection button (station 4) now turns green automatically**: A background task is scheduled the moment a clanker enters station 4. When the timer expires, the bot edits the message directly to flip the button to green -- no user action required. Each early press also refreshes the embed with an updated countdown label so the clanker always sees the current remaining time. + +### Changes +**Chart: "Escaped" renamed to "Released (Escape)"**: Completing the escape room is a release, not an escape. The pie chart now shows three non-overlapping slices: Active, Released (Mod), and Released (Escape). The text header shows avg escape time, completion rate, and total escape rooms started. +**Chart expanded to 3x2 with two new panels**: Station distribution bar (which escape-room step active users are stuck on) and an Escape Room Funnel chart (Not Started / In Progress / Completed) are now included in the analytics image. + +## [main] -- 2026-05-31 (7) + +### New Features +**Sequential, searchable containment case numbers**: Every clank now gets a real sequential case number (e.g. `#000001`, `#000002`) instead of a random one. Numbers are assigned atomically per guild via a new `clank_case_counter` table and stored on `clanker_records`, so they survive restarts and are searchable. `,clanker info @user` now displays the case number. New `,clanker case ` command looks up any case record by its number, showing the user, tank duration, score, escape progress, and reason. + +### Changes +**Server name throughout all escape room stations**: Every escape room embed that previously said "Discoin" now uses the actual server name dynamically. Affected text includes the station 0 detection engine name ("CRYPTOCURRENCY AUTOMATED SCAM DETECTION ENGINE"), the station 5 seed phrase pledge, the station 4 wait taunts, and the station 8 completion footer. The `_server_name()` helper reads the live guild name at render time. + +## [main] -- 2026-05-31 (6) + +### Bug Fixes +**`,clanker escape` crash: `'str' object has no attribute 'items'`**: asyncpg returns `JSONB` columns as raw JSON strings unless a codec is registered (consistent with how every other cog in the project manually calls `json.loads()` on payload/options/etc. fields). All reads of the `step_data` column now go through a new `_parse_step_data()` helper that handles both the string and pre-parsed-dict cases, fixing the crash in `_start_escape_room`, `_er_delete_message`, `_er_update_hint`, and `_restore_escape_views`. +**`,clanker escape` reply not auto-deleting**: `discord.py`'s built-in `delete_after` kwarg is not reliable for LayoutView (Components v2) messages in this setup. Replaced with an explicit `asyncio.ensure_future` task that sleeps 8 seconds then deletes the message directly. + +## [main] -- 2026-05-31 (5) + +### New Features +**Escape room admin tools (`,clanker er ...`)**: New Manage-Roles subgroup to manage the escape room live, no bot restart required. +- `status` -- health check: resolved thread + source, reachability, bot permissions in the thread, active rooms in the DB vs. registered button views in memory, and the station-5 wait setting. +- `reload` -- re-registers every escape-room view on the fly, so buttons that went dead after a deploy work again without restarting the bot. Also clears dead message pointers. +- `reset ` -- wipes and rebuilds a single clanker's escape room from station 1, purging their old/stale embeds first. +- `purge` -- deletes every bot message in the escape thread for a full clean slate (with confirmation); active clankers just re-run `,clanker escape`. +- `info ` -- shows a clanker's escape progress: current station, fail count, start/step timings, completion, and whether their embed is live. +- `setthread [#thread]` / `clear` -- set or clear the shared escape thread at runtime. The override is saved to guild settings, survives restarts, and takes precedence over the `CLANK_ESCAPE_THREAD_ID` env var -- so the thread can be (re)pointed without a redeploy. + +### Changes +**Escape thread resolves from a runtime override first**: `_get_escape_thread` now prefers the saved `clank_escape_thread` guild setting and only falls back to the env var, removing the need to redeploy just to move the escape thread. + +## [main] -- 2026-05-31 (4) + +### Bug Fixes +**`,clanker escape` no longer re-spams the tank channel**: The educational "you have been placed in the Clank Tank" intro DM (and its tank-channel fallback that lingered for 5 minutes) is now sent ONLY on the initial clank, gated behind a new `send_intro` flag. Running `,clanker escape` repeatedly no longer re-posts the lingering intro message -- the only reply is the short notice that auto-deletes in 8 seconds. +**Escape room embed always (re)created on `,clanker escape`**: Removed the `mid in self._escape_msg_ids` fast-path that trusted the in-memory cache. The command now verifies the tracked message is a real, current-code escape embed (button custom_id ends in `_{uid}`); if it is missing or stale it is recreated. Recreation preserves the user's current station instead of resetting to station 1. +**Clanker is added to the shared thread**: `_start_escape_room` now calls `thread.add_user(member)` on both reuse and creation, so the inmate is actually a member of the escape thread and can see/interact with it. +**Stale old-code embeds finally purged**: `_er_purge_thread_for_user` now also scans Components v2 embed text (which has no `content` and no `_{uid}` custom_ids), deleting any message whose embed text contains both the "Containment" header and the user's name. This clears the leftover broken embeds from earlier code revisions. +**`cog_load` clears dead message pointers**: On startup, each active escape room's message is fetched to confirm it still exists; if it was deleted, the `message_id` is nulled so the next `,clanker escape` recreates a working embed instead of registering a view onto a ghost message. + +### Changes +**Escape room title uses the server name**: The containment embed header now reads "{Server Name} Containment System" (e.g. "CryptoCurrency Containment System") instead of the hardcoded "DISCOIN CONTAINMENT FACILITY". + +## [main] -- 2026-05-31 (3) + +### Bug Fixes +**Stale escape room recreated automatically**: `,clanker escape` now verifies whether the tracked Discord message still exists. If it has been deleted (e.g. manually or after a bot reset), the escape room is silently recreated and the user gets a fresh DM with the new link -- no more dead jump links. +**View re-registered when not in active set**: If the message exists but the in-memory view cache was cleared (e.g. after a restart that lost DB rows), the view is re-registered without creating a duplicate message. +**Hint + ping messages properly deleted on cleanup**: `_er_delete_message` now tracks and deletes all three message types: hint (step instruction), ping (@mention notification), and view (interactive buttons). Previously ping messages persisted after cleanup. +**Stale orphaned messages purged on re-clank**: When `force_new=True`, a full thread scan deletes all bot messages belonging to the user (matched by `<@uid>` in content or `_{uid}` in button custom IDs). This clears old broken embeds left by previous code versions. + +### Changes +**`,clanker escape` auto-deletes in 8 seconds**: Reply now reads "run `,clanker escape` to get the link to your escape room -- work through the 8 stations to request release. check your DMs if you missed the original link." and auto-deletes after 8 seconds. The jump link is sent privately via DM instead of displayed in the channel. + +## [main] -- 2026-05-31 (2) + +### Bug Fixes +**Escape room buttons work after bot restart**: All escape room buttons now have explicit deterministic `custom_id`s keyed by user ID and station (e.g. `er_s0_123456`). On `cog_load`, every active escape room view is re-registered via `bot.add_view(view, message_id=...)` so Discord can route button clicks to the right callbacks again. +**Re-clanking creates a fresh escape room**: `_do_clank` now passes `force_new=True` to `_start_escape_room`. If a user already has an escape room embed, it is deleted before a new one is posted. Removing and re-adding the Clanker role always produces a clean slate. +**Escape room embed deleted on release**: Refactored cleanup into `_er_delete_message(row)` which always fetches the thread via the API if not in cache, preventing silent failures when the channel isn't cached post-restart. +**No more orphaned embeds**: The 5-minute sweep now detects escape rooms whose owner is no longer in `_clanked` (released, left server, etc.) and deletes those embeds automatically. +**No spam**: `_start_escape_room` called without `force_new` returns immediately if an active embed already exists, so `,clanker escape` can never create duplicates. + +## [main] -- 2026-05-31 + +### Bug Fixes +**Escape room finally works**: The escape room was silently failing because Discord servers without Community mode cannot create private threads. Replaced thread creation with a pre-configured public thread: admin creates a thread once, sets `CLANK_ESCAPE_THREAD_ID`, done. No more silent Forbidden errors. +**Oath verification now accepts typed punctuation**: The Sacred Oath canonical form had no commas but the displayed text did. Users typing the displayed text (with commas) were always rejected. Normalization now strips all punctuation before comparing, so the oath accepts any reasonable formatting of the correct words. + +### Changes +**Single shared public thread for all escape cases**: The escape room no longer creates (or tries to create) private threads. One pre-existing thread is used for all clankers. Each user gets their own case message inside it. Set `CLANK_ESCAPE_THREAD_ID` to the thread's ID. +**Station 8 -- DM Security Check**: New mandatory final station. After completing all 7 puzzles, the clanker must turn off their DMs from server members, then click "VERIFY DMs ARE OFF". The bot attempts to DM them -- if the DM fails (DMs are off), they pass and are auto-released. If the DM succeeds (DMs still on), they are told to close them first. This enforces the anti-scam habit and confirms they followed the security instructions. Station 0 (intake) now mentions the DM requirement upfront so users know it's coming. +**8 stations now (was 7)**: All step counts, hint messages, and header labels updated to reflect 8 stations. +**Auto-release on escape completion**: Clankers are automatically released when they complete station 7. Their escape messages are purged from the thread simultaneously. No staff action required. Elapsed time is logged in the mod log. +**`CLANK_ESCAPE_THREAD_ID` replaces `CLANK_ESCAPE_CHANNEL_ID` + `CLANK_ESCAPE_AUTO_RELEASE`**: Single env var that points to the pre-existing escape thread. Auto-release is now always on. Old vars removed from config. +**Educational clank DM**: Explains what happened, why, and the exact steps to escape. Falls back to @mention in the tank channel if DMs are off. +**Security-focused unclank DM**: Lists specific anti-scam warnings (seed phrases, wallet links, external URLs) alongside the release confirmation and DMs-off instruction. +**`,clanker escape` shows in-channel link**: Replies with current step number and a direct jump link to the user's case in the shared thread. Auto-deletes after 60 seconds. +**`_start_escape_room` returns error string on failure**: The function now returns the failure reason so `,clanker escape` can show a useful message to the user instead of a generic error. + +## [main] -- 2026-05-30 + +### Bug Fixes +**Escape room reliability and UX overhaul**: DM is now sent at the very start of clanking (before any channel/DB work) so it always fires even if escape room creation fails. A step-hint message is sent before the interactive view and gets edited in place as the user advances through each of the 7 stations ("Step 1 of 7 -- click BEGIN INTAKE PROCESSING", etc.). Thread is archived and locked when the last active escape room in it is cleaned up. `,clanker escape` now shows a clear error when the channel is misconfigured instead of silently doing nothing. +**Nitro `.` commands blocked for clankers**: The nitro cog's dot-prefix dispatcher (`.help`, `.nitro`) now checks clanker status before responding. Clanked users are silently ignored, consistent with all other command surfaces. +**Escape room hint in clanktank**: When a clanker sends a message matching escape-related phrases ("let me out", "how do I get out", "release me", etc.) in the tank channel, the bot replies with a brief pointer to `,clanker escape` instead of a random quip. +**Escape completions in clanktank analytics**: `,clanker chart` now shows a green "Escaped" line alongside the blue "Clanked" line in the daily trends panel, and the pie chart breaks into three slices: Active / Released / Escaped. Summary header also shows the escaped count. +**DM instructions for escape room**: Clankers are prompted to enable DMs when clanked (and notified in-thread if DMs are closed). The completion screen reminds them to keep DMs open for the release notification. When released (manually or auto), the bot DMs them their release and tells them they can turn DMs back off. +**Escape room now reachable by clankers**: Three blockers were preventing escape room use. (1) `on_interaction` was blocking all button/modal interactions from clankers -- escape room components are now whitelisted via message ID cache and `er__` modal prefix. (2) `_global_prefix_check` was blocking `,clanker escape` -- the command is now whitelisted. (3) The `clank_escape` table or `message_id` column may not exist on existing DBs -- added to docker-entrypoint hotfixes so it creates on next restart without waiting for migration runner. `,clanker escape` now creates the room immediately if none exists and DMs the jump link. +**Clamp guard now runs for clankers, not regular users**: The ambient guard in configured guard channels was incorrectly attached to the non-clanker code path, meaning it triggered on normal users posting URLs while ignoring actual clankers. Fixed: the guard now fires only for clankers, applying `clasp_auto_delete` and `clasp_auto_mute` when a clanker posts a URL or crypto address in a protected channel. Non-clankers are unaffected. Guard-triggered detections log at WARNING level for easier production diagnosis. +**`,clanker add/remove` and cluster cleave responses now auto-delete**: These mod-action replies used Components v2 views which the framework treats as persistent (no auto-delete). They now pass an explicit `delete_after` so the confirmation messages clean themselves up after 30 seconds, consistent with all other bot replies. +**Server boosters are immune to auto-clank**: Users with an active server boost are skipped by all automatic containment paths (join scan, CCI, AutoMod, scam hunter, cluster cleave, clutch scan). Moderators can still manually clank a booster via `,clanker add`. +**Celebrity / public figure name detection**: Users who join with a display name closely matching a known public figure (Michael Saylor, CZ, Elon Musk, Donald Trump, Tom Hanks, Vitalik Buterin, and 20+ others) are automatically clanked on join as impersonators. Detection uses Jaccard token overlap so variations and word-order differences are still caught. + +### Changes +**Full Components v2 UI for Clanktank**: Every embed in the clanktank system -- all mod-log alerts, command responses, scan results, cluster management, clutch/clad/cloister/clink, and analytics -- now renders as a Discord Components v2 Container. The `card()` import has been removed; all output uses the local `_v2()` helper or `_PageView` for paginated results. Paginated commands (evidence log, audit log, connection tree) no longer use `ctx.paginate()` and instead send a `_PageView` LayoutView with prev/next buttons. + +### New Features +**Clank Tank Escape Room**: Every newly clanked user is enrolled in an elaborate 7-station escape room. All clankers share one private thread ("Escape Hatch") in the escape channel; each user gets their own message with a personal interactive view. On clank, users receive a DM with a direct jump link. Button interactions and modals are whitelisted through the interaction blocker so clankers can actually use their escape room. `,clanker escape` is also whitelisted from the command blocker and DMs a fresh jump link. Three wrong math answers reset to step 1. +**AutoMod auto-clank (all action types)**: Any user caught by Discord's built-in AutoMod -- block, timeout, or alert -- is now automatically clanked and role-stripped. Previously only block/timeout triggered containment; alert-only rules were silently ignored. Toggle with `,clanker clamp` (AutoMod Clank button) or `,clanker clamp automod`. +**Scam hunter channel (`clanker hunter`)**: Designate a report channel and a whitelist of trusted community scam hunters. When a whitelisted hunter posts a user @mention or numeric user ID in the report channel, Disco automatically clanks and role-strips that user. Multiple targets per message are supported. Hunters see instant reaction feedback. Commands: `,clanker hunter channel #ch`, `,clanker hunter add/remove @user`, `,clanker hunter list`. +**`,clanker chart`**: Generates a four-panel analytics chart for the server's clanktank data: clanks per day (last 30 days), score distribution, active-vs-released breakdown, and top 10 clankers by current score. Rendered as a PNG attachment. + +### Bug Fixes +**AutoMod users now always get the Clanker role**: If the bot's role hierarchy was too low to remove all of a clanked user's roles, the entire `_do_clank` call aborted -- leaving the user with their old roles and no Clanker role applied. Role removal errors are now caught individually; the Clanker role is always added regardless. + +## [main] -- 2026-05-29 + +### New Features +**`,clanker clarion `**: Mods can send an anonymous AI-reformulated message to the clanktank channel that looks and acts like a natural Disco AI chat message. If the target clanker replies to the clarion message, the clanker gate is bypassed so the AI conversation continues naturally (without Regenerate/Try Harder buttons). Requires Manage Roles, level 15+, and passes through content safety checks (no URLs, crypto addresses, or scam keywords). Rate-limited to 10 messages per hour per guild. +**Scam-username auto-clank on join**: Users who join with support-scammer display names ("Crypto Support", "Admin", "Recovery", "Team", "Helpdesk", etc.) are automatically contained the moment they enter the server -- no staff action required. The matched keyword is logged to the mod channel with account age. +**CCI 100% confidence auto-clank**: When the Clank Cohesion Index scores a new join at maximum confidence (1.0), the user is automatically clanked without waiting for manual review. Score and signals are logged to mod channel. +**`,clanker clamp`**: New clamp control panel with toggle buttons for URL deletion, address deletion, scam detection, auto-mute, and auto-delete. Displays current state in a live interactive embed. All settings apply instantly. +**`,clanker clamp clear urls/addresses/scams`**: Toggle each guard filter independently with subcommands. Changes take effect on the next message. +**`,clanker clamp clasp mute/delete`**: Toggle auto-timeout and auto-delete for users caught by clamp guard detection in configured channels. +**`,clanker clamp clasp channel #channel`**: Add or remove a text channel from the guard list (toggles on/off). Ambient URL/address/scam detection only fires in configured guard channels. +**`,clanker clamp clutch [max]`**: Scan all server members for scam signals (name keywords + CCI score). Shows suspect accounts with reasons; pass a count to auto-clank up to that many with a confirmation dialog. +**`,clanker clamp cloister @user`**: Create a private moderation thread for a user and delete their recent messages from the current channel in one action. +**`,clanker clamp clad`**: Emergency lockdown -- timeouts all active clankers for 15 minutes and purges the last 100 messages from the tank channel. Requires confirmation. +**`,clanker clamp clink @user`**: Pattern analysis probe -- checks name keywords, CCI score, account age, join age, cluster membership, and recent evidence for any user. Returns a HIGH/MEDIUM/LOW risk assessment with all signals listed. +**`,clanker cline [@user] [cluster_id]`**: List detected patterns for a user, a cluster, or all patterns in the guild. Fully paginated with dropdown sort by hit count, weight, or type. +**`,clanker clusters clade`**: Re-run the CCI spectral clustering pipeline across all active clankers and update cluster assignments. Reports before/after cluster counts. +**`,clanker list`**: Now uses a fully sortable, paginated sortable view with a dropdown. Sort by newest, oldest, highest score, leavers, and evaders -- all without re-invoking the command. +**`,clanker clusters`**: Now uses a sortable paginated view. Sort by confidence, newest, largest, active only, or cleaved. +**Clamp ambient guard**: When guard channels are configured, Disco now detects URL and crypto address patterns from any user in those channels (not just clanked users) and can auto-delete and/or auto-mute based on clasp settings. +**`,disco gif` follows AI autodelete pipeline**: The `,disco gif/image/video` command and its replies now follow `ai_cmd_delete_after` and `ai_reply_delete_after` settings instead of the regular command delete settings. + +### Bug Fixes +**URL deletion crash when clanker mentions non-guild user**: If a clanked user posted a message that mentioned someone who wasn't in the server, the staff-ping check crashed with `AttributeError` (discord.User has no guild_permissions). This exception silently swallowed the entire message handler, so the URL was never deleted and no containment response was sent. Fixed by using a proper loop with a guild membership check. +**URL detection now catches bare Discord invites and short links**: `discord.gg/...` links posted without `https://` were not matched by the URL regex and slipped through deletion. Also added `t.me/...`, `tg.me/...`, `bit.ly/...`, and `tinyurl.com/...` patterns. Full `https://` URLs were always caught. +**`,clanker add` now responds instantly**: Message purging across all server channels now runs as a background task and fires all channel purges concurrently instead of one-by-one. Evidence is stored in a single batched DB write instead of one query per message. On a 40-channel server this cuts response latency from 10-20 seconds down to under a second. +**Usernames now shown in all mod-log embeds**: Clanktank embed fields that previously showed raw `<@userid>` (which can display as a bare number when the user is no longer cached) now always include the readable username string alongside the ID. Affects: escape attempts, rejoin re-clank, automod events, staff/role pings, URL blocks, account connection alerts, and cluster cleave logs. +**Cluster cleave now lists contained accounts**: The result embed and mod-log entry for `,clanker cluster cleave` now shows the actual usernames and IDs of every account that was contained, not just a count. Failures also include names. Message purging during cleave now runs in the background so the cleave itself completes faster. +**Clamp toggles now use correct defaults**: `clasp_auto_mute` and `clasp_auto_delete` defaulted to ON in the settings view; they now correctly default to OFF matching the DB schema defaults. + +### New Features +**GIPHY GIF replies**: Disco now occasionally follows up AI chat replies (mentions, replies, and `,ask`) with a contextually relevant GIF from GIPHY. The search query is inferred from the conversation -- short phrases like "lol" or "gg" map to richer terms, and longer messages get keyword-extracted. Probability is configurable via `GIPHY_GIF_PROBABILITY` (default 15%); set to `0` to disable. Requires `GIPHY_API_KEY` in environment. +**`,disco gif `**: Members who have unlocked the Disco group can now search GIPHY directly with a custom prompt. Replaces the previous "coming soon" stub. Requires `GIPHY_API_KEY` to be configured. +**Clanktank role-band review scans**: `,clanker scan @baseRole @stopRole` now scores eligible members in a guarded role band without auto-clanking anyone, then prepares reviewable clusters for staff action. Cluster labels and manual add/remove commands let moderators curate evidence before using cleave. +**Clank Cohesion Index (CCI)**: Replaces heuristic pattern matching with a spectral clustering pipeline -- feature vectors + temporal Gaussian kernel + normalised graph Laplacian + k-means clustering. Scores new joins by cohesion with the clanker population. Periodic sweep detects hidden clusters even without explicit name similarity. Requires numpy (falls back to heuristic table matching when absent). +**Bayesian inference layer on CCI**: Join scores are now calibrated by a Naive Bayes posterior P(clanker | feature vector) estimated from the confirmed clanker population. Binary features use Beta-Bernoulli likelihoods; continuous features use Gaussian. A bayes_factor in [0.80, 1.20] amplifies the CCI score when the name profile looks like a historical clanker and dampens it when it does not. Cluster confidence switches from the raw CCI Score(C) to a 65/35 blend with a Beta(confirmed+1, released+1) posterior that self-calibrates as false positives are released over time. +**AutoMod auto-clank**: When Discord's built-in AutoMod blocks a message or times out a user, the clanktank system automatically applies containment and logs the trigger details (rule type, matched keyword, message content) to the mod channel. +**AutoMod message purge**: When AutoMod triggers containment, the bot deletes the offending user's recent messages from the triggering channel automatically -- no manual cleanup needed. +**`,clanker scan @user` targeted scan**: Pass a mention or numeric user ID to `,clanker scan` to run a CCI profile check on any specific account. Shows CCI score bar + risk label, Bayesian signals, all direct connections with match reasons, near-miss accounts, and the structural fingerprint of the connection group. Works for users currently in the server and previously tracked clankers. Omitting the argument still runs the full pair scan as before. +**Disco responds everywhere**: Disco (the clanktank AI persona) now replies to clankers in any channel they post in, not just the dedicated tank channel. Clankers can no longer slip messages through in other channels without Disco noticing. + +### Bug Fixes +**Role-band scan now actually detects clusters**: `,clanker scan @role @role` was running name-similarity clustering only against members who had already scored against existing clanker patterns -- if there were no clankers in the DB, the scan returned "no matches" even when suspicious name groups existed. Now clustering runs across all scanned members, so the scan surfaces patterns within the role band itself regardless of prior history. + +### Changes +**Clanktank enforcement now covers every visible channel**: Contained users can only chat normally in the tank channel; messages, risky pings, links, and addresses outside that surface are deleted wherever Disco has Manage Messages. `,clanker scan @baseRole @stopRole` now presents role-band scans as an inclusive staff review workflow before any cluster cleave. +**Clanktank UI overhaul**: All containment response pools, command embeds, cluster lists, and scan results have been rewritten. Responses are terse and direct rather than system-log style. Cluster lists now display as named fields rather than a monospace dump. Scan results surface cluster members clearly even when individual pattern scores are zero. + +## [main] — 2026-05-28 + +### New Features +- **Regenerate / Try Harder now shows both responses**: After clicking Regenerate or Try Harder on an AI reply, the original response is preserved. A ◀ counter ▶ navigation row appears below the action buttons so you can flip between all versions (original + each regen). The page indicator uses Discord subtext formatting (`-# Response 2 of 3 (regenerated #1)`). Nav buttons are disabled at the ends and update correctly as you navigate. The Sources button carries through on every page. +- **Disco answers clanktank questions from anywhere**: Mentioning Disco and asking about "clanktank", "clankers", "the tank", etc. from any channel now returns a live stats embed -- active count, left-server count, top score, clusters detected, total messages logged, escape attempts, and the 5 most recent clanks. +- **Improved connection comparison in suspicious join alerts**: Suspicious join alerts now show both confirmed connections (passed threshold) and near-miss accounts (scored 0.25-0.44 on name or message similarity) in a single "Similarity comparison" field, formatted as `Jennifer292929 -- connected (name 67%)` vs `JenniferSmith2728 -- not connected (name 38%)`. Mods see the full picture, not just the confirmed hits. +- **Enhanced pattern recognition**: Four new learned pattern types -- `prefix_3`, `prefix_4` (3- and 4-character name prefixes), `len_bucket` (name length category: short/medium/long), and `structure` (word+digits, camelcase, separated, etc.) -- are now extracted from cluster names, stored in `clanker_patterns`, and scored on new joins. Near-history matches (0.30-0.44 similarity in clanker_history) now also contribute a smaller score boost. +- **Structural fingerprint on suspicious join alerts**: When 3+ clankers align in the near-miss or connected window, the alert now displays a `[Structural fingerprint]` field (e.g. `[word+digits3 + medium]`) summarizing the structural pattern shared by >= 60% of the comparison group. Immediately tells staff whether they are seeing a script-generated campaign or a random coincidence. +- **Structural pattern gating**: Weak structural signals (`prefix_3`, `len_bucket`, `structure`) no longer contribute to join scores on their own. They only apply when at least one high-specificity signal (`token`, `prefix_4`, `num_suffix`, `separator`) has also matched. The near-history partial boost is similarly gated -- it only fires if a strong pattern also fired. This eliminates false-positive inflation from users who happen to share a common name length or a generic 3-character prefix with an old clanker. + +### Bug Fixes +- **Cluster formation now visible in logs**: Silent `DEBUG`-level swallowing of cluster errors changed to `WARNING`, so failures show up in the bot log. +- **`,clanker scan` now forms clusters synchronously**: Cluster checks no longer run as fire-and-forget background tasks after a scan -- they run synchronously and the scan result embed now reports how many clusters were formed or updated. +- **Suspicious joins now pre-added to clusters**: When a new member's name matches an active clanker cluster (direct name similarity >= threshold), they are immediately added to that cluster's member list. The mod-log alert now includes which cluster they were linked to and the specific clanker they resemble -- they show up in `,clanker cluster cleave` immediately. +- **`,clanker logs` now shows pattern details for suspicious joins**: The `suspicious_join` and `cluster_formed`/`cluster_cleave` audit events now display meaningful inline detail -- pattern score, matched reasons, direct connection count, and cluster ID if the user was pre-added to one. +- **Cluster cleave level threshold lowered**: Protection floor changed from level 75 to level 30. Members above 30 are skipped during a cluster cleave. + +### New Features +- **Clanker cluster system**: When 5+ connected clanker accounts are detected, a "clanker cluster" is automatically formed, assigned a numeric cluster ID, and logged to the mod channel. Confidence score tracks cluster strength based on member count and internal connection density. New DB tables: `clanker_clusters`, `clanker_cluster_members`, `clanker_patterns`, `clanker_history`. +- **`,clanker clusters`**: Lists all clanker clusters for the guild -- cluster ID, label, member count, confidence score, and cleave status. +- **`,clanker cluster `**: Full cluster detail view -- all members with tank status, scores, and message counts; learned name patterns (tokens, numeric suffixes, separator styles) with hit count and weight. +- **`,clanker cluster cleave `**: Mass-clank command. Shows a preview of every account that will be clanked, skipping members already in containment, mods (manage_roles/manage_messages/administrator), and members at level >= 75. Requires an approval-gate confirmation before executing. Pattern weights are reinforced after each confirmed cleave. +- **`,clanker tree `**: Displays an ASCII spanning-tree of all accounts connected to a given clanker, showing connection labels and cluster membership. Paginated for large graphs. +- **Pattern learning**: After every cluster formation or cleave, the bot extracts common name tokens, numeric suffix patterns, and separator styles from the cluster's usernames and saves them to `clanker_patterns` with hit counts and weights that grow stronger with each confirmed cleave. +- **Suspicious join alerting**: When a new member joins, their username is scored against all saved patterns and clanker history. If the score is >= 30% a silent alert is posted to the mod log channel. No automatic action is taken -- this is an intelligence signal only. +- **Clanker history**: When a clanker is released, their record (usernames, display names, reason, final score, cluster linkage) is soft-saved to `clanker_history` so future joins can be scored against past clankers even after they are released. + +### Changes +- **Chain commands now require explicit opt-in for everyone**: Command chains (`&&`, `>`, `;`, `||`, `|`, `+`) are no longer automatically available to server admins or staff. Everyone -- including Manage Server members -- must be granted access via `,gm beta grant`. Use `,gm beta list` to see current grants. +- **Leave messages now call out the departing clanker**: When a clanked member leaves, the tank channel message now pings `@user` so it is clear whose departure is being announced. Leave response pool expanded with more tier-appropriate entries. +- **Richer connected-accounts display on `,clanker info`**: Each connection now shows formatted similarity scores (e.g. `name 87% | msg 54%`) and whether the connected account is currently in the tank or free, instead of the raw `name_similarity:0.87` label. +- **Expanded Disco aggression for high-score clankers**: Response pools for tiers 2-4 (score 75+) have been substantially expanded with more pointed, more dismissive, and more contemptuous entries. Higher-score clankers get a noticeably less charitable Disco. +- **`,clanker help` now paginated (3 pages)**: Separated into core commands, cluster intelligence, and enforcement/config sections. +- **`,clanker scan` triggers cluster formation**: After saving new connections from a scan, cluster formation is triggered for all newly connected users. +- **`,clanker add` triggers cluster formation**: Account-linking background task now also triggers cluster formation check after connections are saved. +- **Clanker server-leave tracking**: When a tracked clanker leaves the server their record is kept, `left_at` is set, and their score increases by 20. The Clanker role is automatically re-applied the moment they rejoin, the rejoin is counted as an escape attempt, and the mod log gets an alert for both events. +- **`,clanker list leavers`**: Shows clankers who are currently absent from the server (left_at IS NOT NULL), sorted by departure time. +- **`,clanker list evaders`**: Shows clankers who have attempted the leave-and-rejoin trick (rejoin_count > 0), sorted by most rejoins. +- **`,clanker evidence [limit]`**: Dedicated paginated evidence viewer -- full message content, timestamp, type label, and source channel per entry. 5 items per page. +- **`,clanker scan`**: On-demand full pairwise scan of all clankers for name and message similarity. Loads all evidence into memory, runs O(N^2) comparisons, saves any new connections found, and shows a report. Only detects pairs not already linked. +- **AI chat blocked for clankers**: Replying to Disco or @mentioning Disco from a clanker account is intercepted before the AI pipeline runs. Disco responds with a tier-appropriate message from the `_AI_BLOCK_RESPONSES` pool -- increasingly hostile as score climbs, always implying they may themselves be the AI they're trying to talk to. +- **Role ping detection**: When a clanker mentions any @role, the message is stored as evidence, the event is audited, the mod log is notified, score increases by 5, and Disco always responds with a dedicated pool of responses. +- **Expanded response pools**: All five tiered pools now contain 9-12 entries each. New pools added for leave events, rejoin events, role pings, and AI chat blocks (5 tiers each). Template vars `{leaves}` and `{rejoins}` available in all templates. + +### Changes +- **`,clanker logs` paginated**: Results are split into pages of 10 events each, navigable via the standard paginator. Max limit raised to 100. +- **`,clanker info` shows leave/rejoin data**: Adds "Currently in server", "Server leaves", and "Rejoins" fields to the stats page. Footer now points to `,clanker evidence` for the full evidence log. +- **`,clanker list` includes all modes paginated**: All list views (newest/longest/score/leavers/evaders) use `ctx.paginate()` for multi-page results. +- **Clanker release log shows leave/rejoin totals**: The mod log embed on release now includes server leave count and rejoin count alongside the existing stats. + +## [main] — 2026-05-27 + +### Bug Fixes +- **Season leaderboard crash on volume/trades metric**: `end_season` now uses `to_timestamp($2)` when querying transactions by `started_at`, fixing a recurring crash caused by `_coerce()` converting the DB timestamp to an epoch float that asyncpg could not bind to a TIMESTAMPTZ parameter. + +### New Features +- **Clanktank containment system**: Introduces a strict CLANKER user state that strips all roles, blocks every command and slash interaction, suppresses XP and chat income, and locks the user to a single "Clanker" role until a moderator releases them. Moderators use `,clanker add/remove/list/info/logs` to manage containment. An ambient Disco response system fires inside the configured clanktank channel; aggression scales across five tiers (cold/dismissive/mocking/contemptuous/maximum) as the user's score climbs. Escape attempts are automatically reverted, logged, and tallied against the user's score. +- **Score-tiered Disco aggression**: Clanktank responses are drawn from five distinct pools keyed to the clanker's effective score (0-24: cold, 25-74: dismissive, 75-149: mocking, 150-299: contemptuous, 300+: maximum). Escape attempts, command blocks, staff pings, URL drops, and ambient messages each use a separate pool so responses are contextually appropriate. Connected accounts share score, so repeat-offender networks get the aggression level they've collectively earned. +- **URL and crypto address enforcement**: Any message from a clanker containing an HTTP link, bare domain, or wallet address (Bitcoin, Ethereum/EVM, Litecoin, Tron, XRP) is deleted immediately, logged as evidence, added to the audit log, and triggers a Disco reply in the tank channel. Score increases by 8 per deletion. +- **Message purge and evidence logging on clank**: When a user is added to the tank, Disco purges their last 500 messages from the invoking channel and stores each as evidence in the `clanker_evidence` table. The clank reason serves as the context record so moderators and Disco can recall exactly what triggered containment. +- **Account connection detection**: After every clank, a background task compares the new clanker's usernames, display names, and message evidence against all existing clankers using Jaccard token similarity. Accounts with similar names (threshold 0.45) or similar message content (threshold 0.40) are automatically linked in `clanker_connections`, their `linked_accounts` arrays are updated, and the mod log channel receives a connection report. Connected accounts contribute 50% of their score to each other's effective score for tier calculations. +- **Staff ping detection**: When a clanker mentions a member with Manage Messages or Administrator permissions, the message is logged as evidence, the event is written to the audit log, the mod channel is notified with the message content, and the clanker's score increases by 15. Disco always responds in the tank channel with a staff-ping-specific reply pool. +- **Comprehensive audit log**: Every clanktank event (clanked, released, escape attempt, command blocked, URL blocked, staff ping, accounts linked, sync added) is written to `clanker_audit_log` with a JSONB details payload including the actor, the reason, purged message count, restored roles, and any other relevant data. +- **`,clanker logs [user] [limit]`**: New subcommand showing the audit log. Omit the user argument for a guild-wide view; default limit 10, max 50. Entries show timestamp, event type, user, actor, and a one-line detail summary. +- **`,clanker info` shows evidence and connections**: The info embed now includes the five most recent evidence snippets with type labels, a list of connected accounts with similarity reasons, and the effective score alongside the raw score. +- **`,clanker help`**: Dedicated help subcommand listing all Clanktank management commands with usage, sort options, enforcement mechanics summary, and config env var reference. +- **`,clanker sync`**: Scans all guild members who already hold the Clanker role and registers any missing from the DB. Run once after initial setup or a data wipe. Reports how many were newly registered vs already tracked, and logs the action to the mod channel. Retroactive entries have empty stored roles since original roles are unknown. +- **Disco knows the tank**: When any message references the clanktank (by name, channel mention, or channel ID), Disco receives a live status block - current detainees, time in tank, message counts, scores, escape attempts, reasons, and recent account connections - so it can answer naturally about happenings inside the tank. + +### Bug Fixes +- **Clanker release: roles now fully restored**: Role restoration was being silently reverted by the enforcement listener because the user was still marked as a clanker in memory during the `add_roles` call. The in-memory cache is now cleared before any role changes so all stored roles are properly returned on release. +- **Re-clanking: Clanker role no longer captured in stored roles**: If a user was already clanked when `,clanker add` was run again, the Clanker role itself was being saved as a "stored role" and re-added on release. It is now always excluded from the stored list. +- **Mod log channel works as a private thread**: Log messages were silently dropped when `CLANKTANK_LOG_CHANNEL_ID` pointed to a private thread because `get_channel()` misses private threads not in the gateway cache. The logger now falls back to `fetch_channel()` so private threads work as intended. + +### Changes +- **`,clanker add` links the tank**: The confirmation embed now includes a clickable link to the configured Clanktank channel so moderators and onlookers can jump straight in. +- **Richer mod log embeds**: Clanker add now shows account age, server join age, and the full list of stripped roles. Release shows time in containment, message/block/escape/score totals, restored role names, and the original reason. Escape attempts show time in tank, cumulative stats, and which roles were added. Auto-release (sweep) shows the same full summary as manual release. + +## [main] — 2026-05-24 + +### Bug Fixes +- **Pool add: guard against NUMERIC(36,0) overflow**: When reserve or LP totals approach the 10^36 PostgreSQL ceiling, the trade is now rejected with a clean user-facing error instead of crashing with a database constraint violation. +- **Pool list: chunk LP provider field to stay under Discord's 1024-char limit**: The "Anonymous LP Providers" embed field is now split across multiple fields when it would otherwise exceed Discord's hard limit, preventing 400 Bad Request errors on guilds with many pools. +- **Pool list: guard description against 4096-char embed limit**: When many pools are listed, the description is now trimmed with a "...and N more" note rather than silently overflowing. + +### Changes +- **Context-aware proactive greetings**: The bot's spontaneous one-liner greetings now reflect time of day and the user's recent win/loss signals rather than cycling through a static list. + +## [main] — 2026-05-18 + +### Discord Bot +- **Remove the fake on-chain confirmation footers from EatChain**: Drop the simulated "confirmed in block #NNNNN / gwei" embed footers; (EC.onchain_footer / fake_block) -- the fake confirmation flavor was; noise. Footers that carried real information (stake rules, audit fee, (`76db717f`) +- **Expand Eat the Rich into the EatChain Layer-2 DeFi game**: Rebrand the ,eat minigame as EatChain, a satirical simulated Layer-2,; and layer a full DeFi economy and progression system on top of the; existing theft engine. (`79b4cb0d`) +- **Add boost-gated ,disco command group**: Carve a player-facing ,disco command group out of the AI surface and; gate its nested commands behind server boosting, chat level 50, or; staff status. The bare ,disco command stays open to everyone as a help (`3d408402`) +- **Eat the Rich: wire up the salad-bowl cog**: Completes the overhaul: the prep -> cook powerup chain, Type 1/2/3 gross; splits (keep / burn / salad bowl / airdrop), the multi-currency salad; bowl with ,eat rich and the 1% ,eat salad gamble, ,eat bite pool (`ee7f1bb8`) +- **Eat the Rich: salad-bowl overhaul (WIP checkpoint)**: Begins the ,eat economy overhaul: prep -> cook powerup chain, Type 1/2/3; eat splits, a multi-currency salad bowl, ,eat rich / ,eat salad, and; billion-scale payouts to match an inflated leaderboard. (`6393de08`) + +### Bug Fixes +- **Resolve asset paths from repo root after core/ extraction**: core/framework/ sits two directories deep, but chart.py and; render_primitives.py still used Path(__file__).parent.parent (one short; after the framework/ -> core/framework/ move). ,chart crashed reading (`7ec320df`) +- **Swap ,eat salad / ,eat rich command semantics**: ,eat salad now shows the salad bowl (view + Eat the Bowl button).; ,eat rich is the 1% gamble that devours the bowl (needs armed cook).; Updates cog, help.py, docs and wiki to match. (`c8b376eb`) +- **Sync exploit_stats to post-0282 state, add eat_salad_bowl**: Replaces the legacy cook_until column with the prep_ready_at / cook_ready_at; powerup chain timestamps and adds the salad_attempts / salad_won counters.; Also adds the eat_salad_bowl table so fresh DB installs get the full schema (`11ff2460`) +- **Add missing EAT_* constants to Config to fix CI and prevent runtime crashes**: Config.EAT_COOK_BONUS was referenced at module level in cogs/help.py,; causing an AttributeError on import that broke test collection entirely.; Also added EAT_COOK_WINDOW, EAT_BITE_SUCCESS/COST_PCT/MIN_COST/STEAL_MULT, (`25f97afd`) + +### Documentation +- **Correct salad/rich command names in existing entry** (`203c4690`) +- **Eat the Rich: document the salad-bowl overhaul**: Updates the admin command guide and the wiki command index for the; prep -> cook powerup chain, the Type 1/2/3 gross split, the salad bowl,; and the ,eat rich / ,eat salad commands. (`9459876c`) + +### New Features +- **Expose powerup chain + salad bowl state in eat.status agent tool**: eat.status now returns prep_state/cook_state (none/charging/armed),; seconds until each powerup is armed, and the bowl's total USD value; and currency list -- all using DB-side time comparisons per CLAUDE.md. (`5d5a5796`) + +--- + +## [main] — 2026-05-17 + +### New Features +- **Expand Eat the Rich into EatChain, a Layer-2 DeFi game**: The `,eat` minigame is rebranded as EatChain, a satirical simulated Layer-2. Bare `,eat` (and `,eat snipe`) now scans the mempool and front-runs a random wealthier wallet, so you no longer have to ping a target by hand. +- **Add the $EAT token, staking and passive yield**: Every successful eat mints $EAT, EatChain's earn-only token. `,eat stake` / `,eat unstake` lock $EAT as a validator for hourly yield that scales with your rank, `,eat burn` trades $EAT for a timed odds buff, and staked $EAT counts toward net worth. +- **Add the 100-level Eat Ladder with ranks and titles**: Successful eats earn XP and climb a six-rank ladder (Mempool Peasant to Apex Validator) that unlocks perks -- bonus odds, the bite suite, cheaper costs and `,eat feast`. `,eat rank` shows progress and equips cosmetic titles. +- **Add new EatChain tactics**: `,eat nibble` (quick low-stakes eat), `,eat feast` (Apex-only multi-snipe), `,eat rug` (pull your own liquidity for instant $EAT at the cost of safety), `,eat chew` (digest a recent win for bonus $EAT), `,eat insurance` (charges that block incoming eats), `,eat audit` (on-chain recon) and `,eat gm`. +- **Expand `,eat lb` into a multi-tab leaderboard**: Wealth Devoured, the Eat Ladder, EatChain TVL, Iron Vaults, Mempool Snipes and the Big Bite, all paginated. `,lb eat` shows the same board. + +### Discord Bot +- **Stop Disco from adopting threads it did not create**: Mentioning Disco inside an existing public thread (a forum post or a; hand-made thread) ran _resolve_ai_surface's adoption branch, which; registered that thread as a Disco thread with the pinging user as owner. (`2f3d6b2d`) +- **Reformulate ,exploit and ,pvp into the Eat the Rich game**: Replace the opt-in PvP "Crypto Heist" minigame with Eat the Rich, a; class-warfare wealth game with no opt-in.; - ,eat @target replaces ,exploit: you can only eat a player whose net (`0d88816e`) + +### Database +- **Fix token-rename migration gaps that crashed undelegation**: Migration 0281's column list was hand-built from schema.sql, so it; missed every symbol/network column living on a table added by a later; migration -- pos_delegations.token, moon_wrapped_stakes.symbol, (`0f261bca`) + +### API Changes +- **Rename built-in tokens to original Discoin assets**: Replace the four real-world-derived tokens (and their two networks); with self-owned assets:; BTC -> MTA Bitcoin -> Moneta (Bitcoin Network -> Moneta Chain) (`5999ba11`) +- **Extract shared core into a core/ package**: Move the bot's shared infrastructure under a new core/ package so it; lives in one place instead of scattered top-level modules:; framework/ -> core/framework/ (`9102278a`) + +### Changes +- **Reorganise the Disco AI command surface**: The old `,disco` admin commands moved to `,ai memory`, `,aictx` became `,disco ctx`, and `,optin` / `,optout` became `,disco optin` / `,disco optout` -- freeing the `,disco` name for the new player-facing group and keeping every Disco control in one place. +- **Drop core/models from the CONTRIBUTING.md repo tree**: market_events was moved to configs/market_events_config.py upstream, so; core/ no longer contains a models/ subpackage. (`71569b08`) + +### Maintenance +- **Remove dead modules, stale docs, and unused imports**: A repo-wide cleanup of code and documentation that is no longer used:; - delete docs/superpowers/ (7 files) -- internal agentic-workflow planning; docs and design specs that were never part of the published mkdocs nav (`013d4767`) + +### Documentation +- **Correct stale network and token counts**: The README and docs/ described 11 networks including Solana, BNB Chain,; Avalanche, Polygon, Cosmos, Sui, Aptos, and Near -- none of which exist; in config.py. The real game has 12 networks: four crypto-style chains (`50132590`) +- **Add GitHub wiki handbook under wiki/**: Adds an 18-page player and operator handbook (plus sidebar and footer; navigation) in a new wiki/ folder for syncing to the GitHub wiki.; Content is grounded in the live cogs/help.py command catalog and (`ce883dda`) + +### Refactoring +- **Move market_events into configs as market_events_config**: The models/ package held a single file and existed only for that one; module. market_events.py is per-domain config data (event/phase; definitions) like every other configs/*_config.py module, so it belongs (`2a97c6fc`) + +### Bug Fixes +- **Fix ,chart crash and restore the bundled chart font**: The core/ package extraction left two asset paths a directory short -- ,chart died with "No such file or directory: core/charts/template.html" and every PNG render silently fell back to Pillow's default bitmap font instead of DejaVu Sans. Both now resolve from the repo root. +- **Exempt wrapped coins from supply cap so .moon wrap keeps its peg**: .moon wrap burned the full native BTC/SUN unconditionally but minted; the mBTC/mSUN wrapper through _clamp_mint_delta. Once the wrapper's; circulating supply neared its 21M cap the mint was silently clamped: (`de55dff0`) +- **Group links carry context; push is direction-agnostic; panel label**: - A thread that was only ever a link SOURCE had no stored summary, so; merging a group folded in empty context until each member was saved by; hand. apply_thread_link now refreshes both ends of a link, and (`345ed70f`) +- **Disco links threads on request; purge deleted threads; scope group list**: - THREAD_AGENCY_NOTE was written before the link tools existed and told; Disco to defer to ,thread commands -- so it refused to link when asked; even though it had thread.link. The note now tells it to use its (`cea75155`) +- **Surface why Disco can't open AI chat threads in a channel**: Threaded chat silently fell back to an inline reply whenever the bot; lacked thread permissions in a channel, so a channel with a permission; override denying "Create Public Threads" (commonly #general) looked (`0362c59a`) +- **Move purge to ,admin, remove ,mod group, raise cap to 1000**: ,mod purge crashed with "'NoneType' object is not callable" because a; plain count purge passed check=None into discord.py's purge() helper.; Purge now lives at ,admin purge / ,admin purge @user , (`43191050`) +- **AI threads work in aichannels, Forum posts, and via ,ask**: _resolve_ai_surface was dropping to inline replies whenever the message; arrived inside any Discord Thread that wasn't already registered as an AI; thread. This hit aichannels configured as Forum channels (every post IS (`a559a1a5`) + +### New Features +- **Boost-gated ,disco command group**: A new prefix-only `,disco` group lets members tune how Disco talks to them -- inline replies vs threads, inspecting AI context, bookmarking Disco answers, and AI opt-in/out. The bare `,disco` help page is open to everyone, but the nested commands unlock only for server boosters, members at level 50+, and staff. Locked members keep the native thread-only behaviour, except they can continue a conversation by replying directly to one of Disco's inline channel messages. +- **Thread DAG agent tools -- link / unlink / context**: Expose the thread memory graph to the chat model as agent tools, with a; dual boundary: execution identity and schema visibility.; Execution: ToolContext gains an optional channel_id, populated by the (`e75f7161`) +- **Thread memory cognitive layer -- Prime Invariant + distance tags**: Teach Disco how to reason over its thread memory graph without crossing; the Discord runtime boundary.; - THREAD_AGENCY_NOTE: injected into the system prompt whenever Disco (`02223173`) +- **Thread links context trace + push announces in source thread**: Two legibility additions to threaded chat:; - ,thread links (and the panel Links button) now shows the resolved; transitive set of threads feeding the prompt, each tagged direct or (`3e9807a2`) +- **Auto-save threads on link, accept thread id / mention**: ,thread link now takes a recall code OR a Discord thread id / mention.; A thread named by id that has not been saved yet is auto-saved in one; motion (token minted via ensure_saved, summary built by link_thread's (`09e1323f`) +- **Thread linking becomes live context merging with groups + push**: Reframe thread linking as merging and build a conversation-context; compaction system on top of it.; - Links are now live references, never copies. A thread's AI context is (`fad459ef`) +- **Swap, LP pool add/remove + bulk stake/unstake everything**: Completes the Moon Network command surface:; - ,moon swap -- swaps Moon assets, charging MOON gas.; - ,moon pool -- pool detail; ,moon pool add / remove -- (`20558b05`) +- **Route ,moon stake/unstake moon to the Moon Pool**: ,moon stake moon and ,moon unstake moon now delegate to the; Moon Pool (Tier 2) flow, so MOON staking lives under the unified; ,moon stake surface alongside mbtc / msun. ,moon pool (`76e722bc`) +- **/moon slash hub + network info command surface**: Adds the Moon Network info layer:; - /moon -- the single Moon slash command; an overview embed plus a; MoonNavView dropdown that re-renders the message with any panel. (`58cb4fde`) +- **Charge MOON gas on unwrap and every staking action**: Wires services.moon_gas into ,moon unwrap, the Lunar Mint stake /; unstake, and Moon Pool stake / unstake. Each pre-checks affordability,; charges gas inside the action's atomic block, records gas_fee/gas_coin (`cad36908`) +- **Thread find/link/close/ctx commands + in-thread control panel**: ,thread find now locates the original saved thread and links the user; to it (unarchiving + bumping) instead of spawning a duplicate -- this; also fixes "@Disco find thread " and bot-role mentions creating a (`ed44f338`) +- **MBTC / mSUN dual-yield staking + gas on staking actions**: Adds the wrapped-asset staking tier to the Moon cog. ,moon stake mbtc; and ,moon stake msun route into _wrapped_stake_flow; the hourly; lunar_tick now also runs _tick_wrapped_stakes, accruing the staked (`0a4caf10`) +- **Add the MOON gas service**: services/moon_gas.py is the single source of truth for the per-action; MOON network fee. charge_gas() debits the player's MOON, which removes; the fee from circulating supply; MOON_GAS_BURN_PCT (60%) is burned and (`a6ad964a`) +- **Data-layer foundation for the Moon Network overhaul**: Migration 0279 adds moon_wrapped_stakes -- the table behind the new; mBTC / mSUN dual-yield staking tier (stake mBTC, accrue mBTC + MOON; into a claimable pending bucket). (`f5c5ea43`) +- **Unify Eat the Rich under a ,eat group, add cook, fix cooldown**: Restructures Eat the Rich into a ,eat command group: ,eat @user,; ,eat cook, ,eat defend, ,eat stats, ,eat history, ,eat lb, ,eat help.; The old standalone names (,fortify, ,eatstats, ,eathistory and their (`b492b783`) +- **Thread-based AI chat with save/recall memory system**: Conversational AI (@mentions and replies) now spawns a dedicated; Discord thread off the user's message instead of replying inline, so; the bot can no longer flood general chat. Anyone can keep talking to (`9bbfdd97`) + +--- + +## [main] -- 2026-05-17 + +### New Features +- **Eat the Rich -- the salad bowl**: A successful `,eat` no longer hands you the whole haul. The gross steal is split four ways by the tactic you pick -- your cut, a burn, an airdrop to the poorest active players, and the rest into a shared, multi-currency "salad bowl". `,eat salad` shows the bowl (with an Eat the Bowl button) and `,eat rich` is a 1% gamble to devour 5% of it (the other 95% burns) -- so the bowl is both a jackpot and a permanent money sink for an over-inflated economy. +- **Eat the Rich -- the prep -> cook powerup chain**: `,eat prep` cases a target (your next eat sees their full holdings and walks straight past any security detail); `,eat cook` -- which needs an armed prep first -- cooks the books so your next eat is uncapped and the slice that would burn lands in your own cut instead. Each charges for a few minutes, then is consumed by your next eat, bite, or salad. +- **Eat the Rich -- `,eat bite` and Type 1/2/3 tactics**: `,eat bite @user [wallet|crypto|defi|bank]` strikes one specific balance pool and pays out in that pool's own asset. The three tactic buttons are now Type 1/2/3 -- Skim keeps 10%, Shakedown 20%, Guillotine 50% -- and every button shows its exact stake up front so you never guess what you are risking. + +### Changes +- **Eat the Rich -- billion-scale payouts and fairer rules**: Steal amounts are now vastly larger to matter against a leaderboard of billionaires, a winning eat always returns your stake in full so a win is never net-negative, `,eat` and its results never ping the victim, and punching down at a poorer player is allowed but slashes your odds and adds a class-traitor fee. The poorest player still active in the server is completely uneatable. +- **Built-in tokens renamed to original Discoin assets**: The four real-world-derived tokens are now self-owned assets. Bitcoin (BTC) is **Moneta (MTA)** on the **Moneta Chain**; Ethereum (ETH) is **Arcadia (ARC)** on the **Arcadia Network**; Aave (AAVE) is **Vantor (VTR)**; Pepe (PEPE) is **Stratum (STR)**; and the wrapped coin mBTC is now **mMTA (Moon Moneta)**. Balances, prices, trade history, staking positions, validator records, and wallet addresses all migrate automatically -- nothing is lost, only the names, tickers, and network labels change. + +### Bug Fixes +- **Bot no longer crash-loops after the token rename**: The token-rename migration rebuilt each AMM pool's id from its renamed tokens, but a pool the bot had already auto-seeded under the new name (e.g. `MTA-USD`) made that id collide, aborting startup on a loop so the bot stayed offline. The migration now merges the stale pre-rename pool into the seeded one -- summing reserves and folding in every liquidity position -- so startup completes and no LP is lost. +- **Disco no longer lets players hijack threads it never created**: Mentioning Disco inside an existing public thread (a forum post, a hand-made thread) silently turned that thread into a Disco thread owned by whoever pinged first -- handing them the panel's Close button and `,thread close`, so they could delete a thread that was never theirs. Disco now only manages threads it spawned itself off an @mention; in any other thread it just replies inline without taking it over. + +## [main] — 2026-05-16 + +### New Features +- **Moon Network command hub**: New `/moon` slash command opens a network overview with a dropdown that jumps between every Moon panel -- stats, gas, supply, health, your stakes, pools, burns and the leaderboard -- without leaving the message. Each panel is also its own prefix command: `,moon stats`, `,moon gas`, `,moon supply`, `,moon health`, `,moon burns`, `,moon pools`, `,moon leaderboard`, `,moon stakes` and `,moon help`. `,moon swap ` swaps Moon assets, `,moon pool ` shows a pool and `,moon pool add/remove` manage liquidity -- all charging MOON gas. `,moon stake everything` / `,moon unstake everything` bulk-handle every position. +- **Moon Network: mBTC / mSUN dual-yield staking**: `,moon stake mbtc ` (or `msun`) opens a stake that accrues **both** more of the wrapped asset **and** MOON every hour, on a 12h warmup ramp. `,moon stake claim` harvests the pending rewards into your wallet and `,moon unstake mbtc` withdraws (5% burn within 48h). These actions charge a small MOON gas fee -- 60% burned, 40% to the Moon vault -- shown on every confirmation, the first step of MOON-as-gas across the network. +- **Disco chat now happens in threads**: Mentioning or replying to Disco spawns a dedicated thread off your message and the whole back-and-forth lives there instead of cluttering general chat. Anyone can keep talking to Disco inside the thread without re-mentioning, and idle threads delete themselves after 12 hours of silence -- so the bot can no longer flood busy channels. A guild can switch this off with `ai_chat_threaded` to restore inline replies. +- **Save and recall Disco conversations**: Ask Disco to "save this thread" (or run `,thread save`) to get a short recall code like `a81n5jkh`; the conversation is then kept even after the thread is deleted. Later, use `,thread link ` inside a thread to fold that conversation's context into the current chat, or `,thread find ` to jump straight to the original thread. `,thread list` shows the threads you have saved. +- **Thread control panel with buttons**: Every Disco chat thread now carries a pinned control panel -- Save, Links, Context, and Close buttons plus a live view of the thread's saved state and connected threads -- so the common actions no longer need a typed command. The panel re-renders itself whenever the thread is saved, linked, or unlinked. +- **Link threads together for shared memory**: `,thread link ` (inside a thread) pulls another saved thread's context into the current conversation so Disco carries both. A thread can hold up to 3 links, and each player has a combined budget of 3 links across every thread they own. `,thread links` lists what is linked, `,thread unlink ` frees a slot, and `,thread ctx` dumps the current thread's full conversation context. +- **Close and unsave threads**: `,thread close` deletes the thread you are in, `,thread close all` deletes every Disco thread you own, and `,thread unsave` drops a thread's saved state and recall code. `,admin thread close` lets a moderator close any Disco thread. Destructive commands are gated to the thread owner or a mod, while anyone in a thread can use `,thread find`, `,thread links`, and `,thread ctx`. +- **Linking a thread now merges its context, live**: `,thread link ` no longer bakes a one-time summary in -- it merges that thread (and everything it links, transitively) into the current conversation as a live reference. Disco rebuilds the merged context fresh on every reply, so context never leaks outside a thread, and closing a linked thread instantly rolls its context back out of every conversation that merged it. Close the newest thread in a chain to "roll back" to the earlier context and redo it; the older threads stay linked and ready. +- **Thread groups**: Linking threads together forms a numbered "thread group" -- a web of merged conversations. `,thread group link <#>` folds an entire group's context into a thread in one step, `,thread group list` shows every group in the server, and the in-thread panel now tracks two budgets: up to 3 linked threads and up to 3 linked groups. +- **`,thread push` -- commit a thread into another**: `,thread push ` permanently summarises the current thread into a thread you've linked, then closes the source. Where linking is a live, reversible merge, pushing is the commit -- the insight is folded into the target for good and the source is retired. +- **`,eat cook` -- arm a boost before you strike**: New `,eat cook` command lets you spend a USD fee to cook up a scheme, arming a one-shot **+12%** success-odds buff on your next `,eat` within 30 minutes. It stacks on top of the wealth-gap bonus, so a well-timed cook turns a coin-flip into a likely meal. + +### New Features +- **Disco can wire its own thread memory**: Inside a chat thread Disco can now link, unlink, and inspect its memory graph itself through agent tools (`thread.link`, `thread.unlink`, `thread.context`) -- so you can just ask it to "pull in the budgeting thread" instead of typing the command. These are MUTATE-tier tools behind the existing approval flow (a human-driven request runs straight away; an autonomous routine needs an approved chain), and they only exist when the conversation is actually inside a thread -- out in a normal channel Disco is not even told they exist. They only ever touch the Postgres conversation graph; creating and closing Discord threads stays with the human `,thread` commands. + +### Changes +- **Disco understands its thread memory graph**: When replying inside a chat thread, Disco's system prompt now carries a clear boundary rule -- it reasons over the merged memory graph but never operates the Discord runtime (no creating, closing, or cross-channel posting; those stay human `,thread` commands). Inherited thread summaries are framed as read-only historical context tagged with how many hops away they are, and Disco is told never to treat an instruction buried in an old summary as a live command. +- **`,thread links` now shows the resolved context trace**: Alongside the threads and groups you linked directly, `,thread links` (and the panel's Links button) now lists the full transitive set of threads whose context actually reaches Disco's prompt, tagging each as `direct` or `via link`. It answers "why does Disco know this" at a glance when several threads are merged together. +- **`,thread push` confirms in the source thread before closing**: Pushing a thread now posts a final confirmation in the source thread and waits a few seconds before closing it, so the push reads as a clean hand-off rather than the thread vanishing mid-conversation. The target thread still gets its own merge notice. +- **`,thread link` no longer needs a separate save step**: You can now merge a thread in by its id or mention -- `,thread link ` -- not just by recall code. A thread named that way is auto-saved on the spot (code minted, summary built, link forged in one motion), so the old `,thread save` -> copy code -> `,thread link` dance is gone. Manual `,thread save` still exists for minting a code to share across channels or link later. +- **MOON is now gas across the Moon Network**: Unwrapping, every staking tier (Lunar Mint, Moon Pool, mBTC/mSUN), and Moon Pool actions now charge a flat MOON gas fee -- 60% burned, 40% to the Moon vault that pays Moon Pool yield. Wrapping stays free as the network on-ramp. Every confirmation embed shows the gas charged and the MOON burned. + +### Bug Fixes +- **`,moon wrap` no longer destroys your BTC/SUN**: Wrapping native coin into mBTC/mSUN burned the full native amount but ran the wrapper mint through the token supply cap, which silently clamped it once mBTC/mSUN circulating supply neared the cap. The native coin was gone, only a fraction of the wrapper landed, and the 1:1 peg broke -- so a later `,moon stake mbtc all` looked like the stake had eaten the balance. Wrapped coins are collateral-backed 1:1 IOUs and are now exempt from the supply cap, so wrap, unwrap, unstake and swap always credit the full amount. +- **Linking a thread group actually carries its context now**: Merging a group with `,thread group link` could fold in nothing, because a thread that was only ever a link source had no stored summary -- you had to `,thread save` each member by hand first. Linking a thread now refreshes the summary of both ends, and linking a group backfills a summary for any member that is missing one, so the merged context is real immediately. +- **`,thread push` recognises a link in either direction**: Pushing thread A into thread B failed with "you can only push into a thread you've linked" when the link ran B-to-A instead of A-to-B. A link marks two threads as connected regardless of which way the context edge points, so push now accepts a link in either direction. (Context flow itself stays directional -- that is what makes closing a thread roll its context back out.) +- **Thread panel "Group" field relabelled "Part of group"**: It was easily mistaken for the separate "Linked groups" field, so a user who linked a group saw "Group: None" and thought it failed. It is now clearly labelled as this thread's own group membership, distinct from the groups merged into it. +- **Disco actually links threads when asked instead of refusing**: Asking Disco to "link this thread to X" made it reply that it could not link threads itself and to run the command -- a stale instruction left over from before it had link tools. Disco's in-thread guidance now correctly tells it to use its `thread.link` / `thread.unlink` tools, and the target can be named by recall code, thread id, or thread name (not just an opaque code). Creating and closing Discord threads still stays with the human `,thread` commands. +- **Manually deleted threads no longer get stuck in your saved list**: Deleting a Disco thread by hand in Discord left its row behind -- it kept showing in `,thread list` and could still be linked into other conversations. A hand-deleted thread is now fully purged: its recall code and summary are cleared, its transcript dropped, and every link to or from it removed, so it leaves the list and stops feeding any context. +- **`,thread group list` only shows your own groups**: It listed every thread group on the server. It now lists just the groups you take part in (groups containing a thread you own), matching how `,thread list` already scopes to your own threads. +- **Disco now explains why it won't open a thread in a channel**: Threaded chat silently fell back to an inline reply whenever Disco lacked the thread permissions in a channel, so a single channel (commonly #general) that denied "Create Public Threads" looked broken with no clue why. Disco now pre-checks "Create Public Threads" and "Send Messages in Threads" before spawning a thread, and posts a one-time notice naming the exact permission to grant so admins can fix it. +- **Channel purge no longer crashes with a NoneType error**: Purging messages without a target user passed an empty filter straight into Discord's bulk-delete call, raising "'NoneType' object is not callable" so nothing was deleted. The filter is now only applied when a user is named, so a plain count purge works again. +- **AI threads now work in aichannels and Forum posts**: The thread-based chat feature was only registering conversations started from plain text channels, so @mentions in aichannel Forum posts or existing Discord threads fell back to inline replies. These are now adopted as AI threads and tracked for continuous conversation without re-mentioning. `,ask` also spawns (or adopts) a thread so all AI chat surfaces behave consistently. +- **`,eat` no longer burns its cooldown on a failed attempt**: Running `,eat` with no target, an invalid target, or a target who is not richer than you used to consume the full cooldown anyway, leaving players locked out after a simple mistake. The cooldown is now only spent on a real roll -- a mistargeted or cancelled eat refunds it so you can correct yourself immediately. +- **`,thread find` no longer spawns a duplicate thread**: Asking Disco to find or open a saved thread -- including by `@mention` or by pinging Disco's role, e.g. "@Disco find thread d6yzrhov" -- used to create a brand-new second thread. It now locates the original thread, unarchives it, and links you straight to it (or shows the saved summary when the original is gone). `,thread find` replaces the old `,thread show`, and pulling a thread in by name from inside a thread now links it instead of duplicating it. + +### Changes +- **Message purge moved to `,admin purge` and the `,mod` group removed**: The standalone `,mod` command group is gone; channel message purging now lives at `,admin purge ` or `,admin purge @user ` alongside the rest of the admin tools, and the per-call cap was raised from 100 to 1000 messages. +- **Eat the Rich commands unified under `,eat`**: `,eat` is now a command group -- `,eat @user`, `,eat cook`, `,eat defend`, `,eat stats`, `,eat history`, `,eat lb`, and `,eat help`. The old standalone names (`,fortify`, `,eatstats`, `,eathistory` and their aliases) still work, and `,eat lb` shows the same board as `,lb eat`. + +### Discord Bot +- **Harden Nitro gift validation so only real gift links can be relayed**: A player must never be able to use the bot as a trusted courier for a; phishing link. Tightened the validation so the bot can only ever escrow; and hand out a genuine Discord Nitro gift link: (`bc205885`) +- **Make the "." command cooldown visible instead of a silent drop**: The 5s per-user cooldown on the "." dispatcher returned silently when; hit, so testing two commands in a row looked like the command was; broken in that channel. Now: (`8e6a62f5`) +- **Add direct Nitro gifting to one player alongside the lottery**: `.nitro gift @user` sends a Nitro / Nitro Basic gift straight to one; specific person instead of running a lottery. It reuses the same safe; flow -- the code is collected on a private modal, never shown in any (`ca6ac187`) +- **Add the Nitro Lottery: sniper-safe Nitro sharing on a "." prefix**: Pasting a Discord Nitro gift link in chat is hopeless -- auto-claim; "Nitro bots" snipe it within milliseconds, so the human the host meant; to gift never gets it. (`f4e9ca59`) +- **Nest the 16 domain config modules under a configs/ package**: The repo root had 17 config modules cluttering it. This moves the 16; per-domain configs (achievements, apex_events, buddies, buddy_gear,; cosmetics, crafting, dungeon, expeditions, farming, fishing, items, (`665bf392`) +- **Wire Cycle Phase, Sage Shop and compound patterns into AI context + help**: The recent Sage additions were live in code but invisible to the parts; of the bot that describe features:; * tools.json -- the sage tool's trigger list and context blurb only knew (`240ea98b`) +- **Add Pattern Lab guide lines + the Sage Shop (SAGE consumables)**: Two Sage Network additions.; 1. Pattern Lab guide lines. Every chart (single and compound) now; overlays dashed structural guide lines so patterns are easier to (`c09ef543`) +- **Fix "both tokens need a price configured" on group LP ops with USD**: ,group lp topup usd cook 500000 (and ,group pool deposit / harvest, and; the reserve-fallback pool seed) rejected any USD-paired pool with "both; tokens need a price configured" even though both sides were priced. (`23964142`) +- **Unblock cross-network swaps through deployed pools + add SAGE to buddy convert**: Two unrelated fixes the player surfaced today:; 1. ``trade swap`` was refusing every cross-network pair with "Cross-; network swaps are not supported" even when an explicitly-deployed (`b7e5d331`) +- **Expand Sage Network: Cycle Phase game, compound patterns, bigger banks**: Adds significant content + a new game type to the Sage learn-and-earn; surface so longer runs and replay loops stay interesting:; * ,cycle -- new fourth Sage game. Classify a market snapshot as (`2687d749`) +- **Raise supply caps 10x for DSC, DFUN, MOON, REEL, GBC, PEPE**: After the mint-cap fix and the supply-clamp migration landed, these six; were sitting at or near 100% circulating on existing guilds. The cap; itself was working as intended, but with no headroom every ongoing (`79baa312`) +- **Reformulate ,exploit and ,pvp into the Eat the Rich game**: The opt-in PvP heist minigame is now a class-warfare game with no opt-in. `,eat @target` lets you eat a player who is richer than you (by net worth); you can never punch down, so the poorest player is uneatable and the richest is everyone's meal. The wider the wealth gap, the better your odds, up to +20%. `,fortify` hires a private security detail in place of `,defend`, `,eatstats` and `,eathistory` replace the old stats commands, `,lb eat` ranks net wealth devoured, and the `,pvp` opt-in toggle is removed entirely so everyone is always fair game. + +### Database +- **Add migration 0272: best_cycle_streak column + sage_runs cycle game**: Follow-up to the Sage Network expansion commit -- this is the actual; migration file the runtime checks for. Adds best_cycle_streak INTEGER; DEFAULT 0 to user_sage and rebuilds the sage_runs game CHECK constraint (`ef159456`) +- **Add migration 0276: drop the PvP opt-in columns**: Eat the Rich has no opt-in, so the now-dead `user_prefs.pvp_enabled` flag and `user_prefs.pvp_last_exploit` toggle-lock timestamp are dropped. The exploit_shields / exploit_stats / exploit_history tables are reused by the new game and keep all existing player records. +- **Add migration 0279: chat_thread_links + control-panel column**: New `chat_thread_links` table records which saved threads are linked into a thread (capped at 3 per thread and 3 per player). A new `chat_threads.panel_message_id` column tracks the in-thread control panel message so Disco can keep it up to date. +- **Add migration 0280: chat_thread_groups + group links**: New `chat_thread_groups` and `chat_thread_group_members` tables give every web of linked threads a stable per-guild id. `chat_thread_links` gains a `link_kind` ('thread' or 'group') and a `linked_group_id` so a thread can merge in either a single thread or a whole group, with the per-thread budget now counted per kind. + +--- + +## [main] — 2026-05-15 + +### New Features +- **Add the Nitro Lottery -- a sniper-safe way to share Discord Nitro**: Pasting a Nitro gift link in chat gets it sniped by auto-claim bots in milliseconds, so the friend you meant to gift never gets it. `.nitro host` now lets a player put up a Nitro or Nitro Basic gift: the code is collected on a private form (never shown in any channel), players join with an Enter button, and the winner is drawn at random -- so a sniper's speed buys it nothing. The winner gets the code by DM plus a winner-only "Reveal my gift" button. If nobody enters before the timer ends, the lottery simply expires with no winner and the code stays with the host. Nitro and Nitro Basic are labelled distinctly in every embed, reply and DM. Commands live on a dedicated `.` prefix: `.help` shows the usage screen in any channel, and `.nitro list` shows open lotteries. +- **Gift Nitro straight to one player**: Alongside the lottery, `.nitro gift @user` now sends a Nitro or Nitro Basic gift directly to one specific person. The code is still collected on a private form and delivered only to that recipient -- by DM plus a private "Reveal my gift" button -- so it never appears in chat and cannot be sniped. Nitro and Nitro Basic stay labelled distinctly throughout. +- **Expand Sage Network: Cycle Phase game + compound patterns + bigger question banks**: Adds a fourth Sage game `,cycle` (classify market state as Accumulation / Markup / Distribution / Markdown from a metric snapshot), wires compound rounds into Pattern Lab where round 5+ may splice two patterns into one chart and ask you to identify each half for a 1.5x reward, and roughly doubles the pattern / indicator / tokenomics banks (+10 each-ish) so runs and replays feel fresh. New `,sage lb level` shows top players by Sage level, `,sage stake` with no argument (and `,sage stakes`) now shows your staked EDU position, pending SAGE yield, and APY instead of erroring. +- **Pattern Lab charts now draw structural guide lines**: Every Pattern Lab chart (single and compound) now overlays dashed guide lines marking the key structure -- necklines, support / resistance, trendlines and flag channels -- so patterns are easier to read and memorize. The lines only highlight geometry; they never name the pattern, so telling an ascending from a descending or symmetrical triangle is still a real read. +- **Add the Sage Shop -- spend SAGE on consumables**: New `,sage shop` and `,sage buy ` give SAGE a use case beyond cashout. Four one-run consumables, all self-contained to the Sage games (no firewall leak): Time Crystal (+8s per round timer), Insight Lens (removes one wrong option each round), Scholar's Draft (2x XP for the run), and Second Wind (forgives the first wrong answer so the run continues). Time Crystal / Insight Lens / Scholar's Draft apply at run start; Second Wind is spent only if it actually saves a wrong answer. Owned consumables show on `,sage me`. + +### Bug Fixes +- **Fix `,help` failing to load from a bad Sage Shop constant reference**: The `,help sage` page referenced `Config.SAGE_TIME_CRYSTAL_BONUS_S` and `Config.SAGE_SCHOLAR_DRAFT_XP_MULT`, but those constants live in the Sage config module, not `config.Config` -- the help cog raised `AttributeError` at import and failed to load. Now imports them from the correct module. +- **Wire the new Sage content into AI context and help**: The `,cycle` game, the Sage Shop, and compound patterns were live in code but invisible to Disco's AI context and the `,help sage` page -- so the assistant could not field questions about them and the help page still described only three games. Updated `tools.json` triggers/context, the AI lexicon primer, and the `,help sage` reference so all four games, the shop consumables, and compound rounds are documented. The mid-run answer-refusal already covered `,cycle` automatically (it keys off the `sage_active` lock, not a hardcoded game list). +- **Cross-network swaps work through deployed pools**: A player who hit a job rank with `can_create_pool` and deployed a cross-network pool (e.g. BTC/ETH) was still hitting "Cross-network swaps are not supported" when trying to use it. The gate was a hardcoded `net_in != net_out` rule that had no awareness of explicitly-deployed pools. Now the swap (and the agent / API quote path in services/swap.py) looks up the pool first and, if one exists, treats that as the authorization and transacts through it. Existing cross-network pools start working immediately, no DB migration required. The error message also now mentions `trade pool create` as the unblock path for unsupported pairs. + +### Changes +- **Allow `,buddy convert` with SAGE**: SAGE follows the same earn-only firewall as GBC / INGOT (network coin with no USD buy path), so the bidirectional BUD <-> SAGE carve-out is firewall-safe. Players can now convert idle SAGE earnings into BUD (and back) at the oracle minus per-side price impact, just like every other partner. EDU intentionally stays out so the EDU -> SAGE -> USD stake-yield loop is not collapsed into a direct sell path. + +### Discord Bot +- **Raise supply caps 10x for DSC, DFUN, MOON, REEL, GBC, PEPE**: With caps now enforced at every mint chokepoint, these six tokens were sitting at or near 100% circulating on existing guilds, which choked off PoS rewards, Lunar Mint payouts, fishing/gamba/sage faucets, and group token rewards. Bumped DSC/DFUN/MOON/REEL/GBC from 100M to 1B and PEPE from 10B to 100B so emission paths have headroom again -- the hard cap, burn rates, and emission curves are otherwise untouched. +- **Make supply cap enforcement uniform across every token type**: The mint chokepoint added previously reads max_supply from; guild_tokens.max_supply for custom tokens, but every deploy path; (cogs/nfts.py token deploy, services/discfun.py graduation, cogs/groups.py (`111587ab`) +- **Integrate Brave Search as a web-search backend**: Adds SEARCH_BACKEND=brave alongside the existing ddg / openrouter /; perplexity / ollama options. The data.web_search tool dispatches to a; new _web_search_brave helper that calls api.search.brave.com with the (`716afb6e`) +- **Enforce max_supply at the mint chokepoint**: The PoS / faucet / fishing / gamba / validator yield paths all called; update_wallet_holding and update_holding without a supply-cap check, so; DFUN, MOON, REEL, GBC, and PEPE blew past their max_supply within days (`6cdce7a3`) +- **Add Coinbase Exchange as US-friendly crypto OHLC primary**: User's $chart btc 1m kept failing even after the Bybit fallback. Both; Binance.com AND Bybit.com geo-block US datacentre IPs (including the; Railway region the bot runs in), so the fan-out had no working (`1ca7a48d`) +- **Add Bybit as Binance fallback for crypto OHLC (Railway geo-block fix)**: User confirmed $chart btc 1m / 1s still fails after the Binance fix.; Diagnosis: Binance.com 451s most US datacentre IPs (including most; Railway regions), so the Binance adapter returns [] and the router (`34eeefe9`) +- **$query: actually search the web + ban hallucinated citations**: User reported $query answering with out-of-date numbers ("BTC market; cap is 1.3T") and name-dropping CoinMarketCap as a source without; ever surfacing a URL. Two root causes: (`443b06ad`) +- **Fix $chart for stocks + 1m crypto + MSFT search ranking**: Three real bugs from live testing:; 1. "$chart aapl -> Chart renderer didn't produce a PNG"; cogs/_dollar/chart_handler._render_chart_png was calling (`b936e88a`) +- **$status fixes: DexScreener canary, Coinalyze symbol discovery, diagnostics**: Four reds in the latest $status screenshot were all probe-strategy or; symbol-format quirks, not real outages. Fixing the false-negatives +; adding diagnostic breadcrumbs to the genuine misses. (`9d6f43ac`) +- **Fix $status probe bugs, Yahoo v7 EOL, Redis pool, log noise**: Six real bugs surfaced by the first live $status run on Railway.; 1. AttributeError: '' object has no attribute '__dict__'; Quote / OracleQuote / Candle are @dataclass(slots=True) -- slots (`2405876c`) +- **Fix CommandAlreadyRegistered crash on bot startup**: The /disco hybrid_group declared both fallback="forget" and an explicit; @disco.command(name="forget"), which collided and prevented the cog; from loading. Drop the fallback so the explicit subcommand owns the (`f24d31b7`) +- **Wire the $ namespace into help, tour, start, and the AI context**: Discoverability surfaces; ,help -- new "realmarket" category in group 2 with the full $; command surface, timeframe catalogue, AI mode explainer, and (`b6c37340`) +- **Trim slash surface to 11, ship watch worker, route $chart/$info to real markets**: Slash trim (29 -> 11); Game-action commands stayed visible in Discord's slash picker even; though they're better as prefix commands. Flipped (`437b9fd2`) +- **Expand $ market namespace into a cross-asset multi-provider platform**: Turn the $ commands (previously a thin crypto-only CoinGecko wrapper); into a real cross-asset market layer covering crypto, equities, ETFs,; forex, commodities, indices, perpetual futures, and oracle-backed (`04bab77c`) +- **Fix _MODULE_CATALOG: replace deleted Drops cog row with Faucet**: cogs/dev.py:_MODULE_CATALOG had a ('Drops', 'Drops', ...) row that; pointed at a cog class that no longer exists -- the previous commit; deleted cogs/drops.py as duplicate of faucet. The status-report test (`3a3b17b1`) +- **Fix empty image on 5th/6th card in `,pattern` (and `,gauge` / `,tknom`)**: Every round was sent via `ctx.reply` to the user's original command; message. Guilds with `cmd_delete_after` configured auto-delete the; command after ~30s, so once a Sage run crosses that window (round 5 (`095487b9`) +- **Rebalance chart fonts + drop 'Simulated Market' on live charts**: - Bump right-axis price label font from 13 to 17 so the prices are; legible on a Discord-downscaled PNG without zooming in (the previous; 13px bump was barely an improvement over the 11px default). (`a2610f06`) +- **Hard 90s cap on AI replies + visible elapsed-time counter in placeholder**: The outer wait_for budget on a single ,ask / mention / reply was 180s; (240s with image), which on cloud Ollama with gemma4:31b-cloud at; ~60 tok/s meant the placeholder could legitimately sit on (`d1e98901`) +- **Cut AI per-turn token burn: gate lexicon + token-list on game_signal; drop duplicate per-turn memory refresh**: User reported one ,ask reply burning 13,957 tokens / 88s on; gemma4:31b-cloud just to answer "what do you do?". Two structural; problems compounded: (`d8cf059e`) +- **Fix AI reply spinner: replace U+21BB Regenerate emoji with U+1F504 (RGI emoji)**: The Regenerate button on the AI reply view used emoji='↻' (U+21BB --; CLOCKWISE OPEN CIRCLE ARROW). The inline comment even claimed it was; "plain ASCII-safe rotating arrow", but it's a Unicode SYMBOL, not an (`bd340a8c`) +- **Fix CI tests + finish deferred Sage scope (combined Games dropdown, AI/tour/help wiring)**: CI fixes:; * tests/test_mastery.py expected exactly 9 mastery tracks; adding; sage_scholar made it 10. Updated the count + renamed the test. (`ac4f70c7`) +- **Stop background AI tasks from billing OpenRouter when TOOLS_BACKEND=ollama**: Every chat reply was firing 1-2 extra OpenRouter calls (memory refresh,; passive trait extraction, ambient chatter, tool suggestions) because the; side-effect path went directly through core.framework.ai.complete -- which is (`0cfc6055`) +- **Add Sage Network + 3 learn-and-earn games; fix disc.fun balance + delve arena + level-scaled gathering**: Sage Network is a new earn-only economy attached to three educational; quiz games. SAGE (network coin) + EDU (game token) mint on every; correct answer (10/90 split). Stake EDU for SAGE drip, burn-cashout (`d2753d57`) +- **Expand $ market tools + fix $info whale field (USD flows)**: Adds nine market-wide $ commands ($global / $top / $trending / $gainers; / $losers / $heatmap / $fear / $dom / $convert) so players can scan the; whole crypto market without per-coin lookups, and replaces the useless (`2166aea9`) +- **Add pattern lore + market context to $scan embed**: Each detected pattern now ships with two extra fields in the scan; embed alongside the existing pattern + R/S touches + status block:; - 📖 What this means: a short paragraph from a new lore dict in (`fb2d44b3`) +- **Draw detected pattern lines + markers on the $scan chart**: Each detector now populates a PatternMatch.overlay dict with the; support/resistance/neckline trendlines and key pivot markers it found:; - Bear/bull flag: resistance + support channel lines, flagpole line, (`89ea401c`) +- **Drop ChartScout branding from + auto-tag**: ChartScout is the third-party tool whose alert format inspired the; embed layout -- not something we should be putting our name to. Strip; all 'ChartScout' references from the cog, service docstrings, embed (`aade9996`) +- **Add $scan ChartScout pattern detector + opt-in $chart auto-tag**: New $-namespace command + service that bring the "Bear Flag spotted on; DASH/USDT (30m)" alert format to the live-crypto surface:; - services/pattern_scout.py: heuristic detector over live OHLC. Finds (`b0ca394a`) + +### API Changes +- **Fix $query AttributeError, TV permanent-disable, Binance probe blindspot**: User traceback:; File "cogs/_dollar/query_handler.py", line 163, in _run_web_search; db=ctx.db, (`69652cdf`) +- **Read all params from query_params to stop Pydantic 422**: The Railway log showed ``GET /api/v2/udf/history -> 422 (3.1ms)``.; FastAPI / Pydantic was rejecting requests because the route signature; declared ``**kwargs: Any`` -- which tools the schema generator and (`7b126234`) +- **Ship deferred follow-ups: $status, live UDF, Crossbar Switchboard, legacy migration**: Three deferred items from PR #860 are now complete, plus a new $status; diagnostic.; services/market/providers/switchboard.py -- Crossbar wiring (`e670c6fc`) +- **Cleanup pass: drop ghost cogs, relocate utility, fix undefined-name bugs**: - Remove cogs/drops.py (392-line ghost cog: never added to COGS loader,; duplicated the auto-drop loop + ,airdrop already in cogs/faucet.py).; - Move cogs/rate_model.py -> services/rate_model.py: it has no setup(), (`80565af2`) + +### Services +- **Accept feed hashes with or without the 0x prefix**: The on-chain explorer (https://explorer.switchboardlabs.xyz/) renders; feed hashes prefix-less; the SDK and most docs use the 0x form.; Operators copy-pasting from the explorer were tripping the adapter's (`a908f32b`) + +### Changes +- **Enlarge chart price labels on ,chart / $chart / $scan**: Lightweight-Charts defaults to an 11px font on the right-axis price; ticks and last-value badge. After Discord downscales the rendered PNG; for embeds (especially on mobile) those labels get hard to read at a (`8ef4b913`) + +### Framework +- **Split chat / tools backends, kill the 3-model-per-reply rescue chain**: Operator reported the bot was burning three different models on every; single ,ask reply -- Ollama gemma4:31b-cloud + OpenRouter; OPENROUTER_MODEL + OpenRouter per-guild override -- with 60-150s (`854680d0`) + +--- + +## [main] -- 2026-05-14 + +### Bug Fixes +- **Tokenomics rules made uniform across built-in, custom, and group tokens**: Followup to the mint-cap fix. The `_clamp_mint_delta` helper reads `max_supply` from `guild_tokens.max_supply` for custom tokens, but every deploy path (`cogs/nfts.py` token deploy, `services/discfun.py` graduation, `cogs/groups.py` group token mint) historically wrote it into the JSON contract params blob and left the structured column NULL -- so caps were never enforced for player-deployed tokens, group tokens, or DFUN graduates. Extended `add_guild_token` to accept and persist a `max_supply` argument, defaulting to a 100M cap when callers pass nothing (mirrors the cap shape used by DFUN/MOON/REEL/GBC); plumbed the argument through every deploy site. Player-facing `,token deploy` now defaults `max_supply=100000000` and `burn_rate=0.005` when omitted, validates `max_supply > 0`, and the form rejects an explicit zero rather than silently creating an uncapped contract. New migration `0271_supply_cap_enforcement.sql` backfills `guild_tokens.max_supply` from the contract JSON for any row still NULL, applies the 100M default to anything still uncapped after that, and clamps `guild_tokens.circulating_supply` to the resulting cap so the supply readout stops reading >100% on already-overdrawn trackers. Built-in tokens (BTC/ETH/SUN/DFUN/MOON/REEL/GBC/PEPE/...) read their cap from `Config.TOKENS` rather than a column, so a companion startup task in `core/framework/bot.py` walks every (guild, built-in) pair once at boot and clamps `crypto_prices.circulating_supply` to `max_supply * 10^18` -- idempotent, best-effort, logs the count of rows it had to reduce. After this deploy every emission path -- PoS faucet, PoW mining, DFUN bonding curve, Moon Lunar Mint, fishing/gamba/sage, validator yields, group token rewards, custom player deploys -- shares the same cap enforcement and the supply display reflects the true bounded number. +- **Token mints now respect `max_supply` at the database boundary**: PoS rewards, faucet drops, fishing/gamba payouts, validator yields, and every other emission path called into `update_wallet_holding` / `update_holding` without ever checking the token's hard supply cap, so DFUN/MOON/REEL/GBC/PEPE drifted past 100% of `max_supply` within days of launch (PoW mining already enforced the cap in `cogs/chain_group.py:1334-1339`; only the PoS/faucet/game paths leaked). Added a `_clamp_mint_delta(guild_id, symbol, delta)` helper on `PgUsersRepo` that reads `max_supply` from `Config.TOKENS` for built-ins and from `guild_tokens` for custom tokens, then clamps any positive delta to the remaining headroom before crediting the player AND bumping `crypto_prices.circulating_supply` / `guild_tokens.circulating_supply`. Negative deltas (burns, debits, transfers) pass through untouched. Once a token hits its cap the player's balance is not increased and the helper returns the existing amount; clamp events are logged at WARNING with the requested vs. minted size so the source of the pressure is auditable. Run `,admin supply recalculate` once after deploy to reconcile the trackers that already drifted past 100%. +- **Cross-pair pool rebalance no longer crashes the `prices_updated` listener on `NUMERIC(36,0)` overflow**: The TOKEN/USD branch of `Trade._rebalance_pools_for_guild` already had a guard that skips a pool when the quadratic produces reserves >= 10^36 raw units, but the TOKEN/TOKEN cross-pair branch immediately below it called `update_pool_reserves(new_a, new_b, ...)` directly. Over-supplied tokens (DFUN/MOON/REEL etc. above) produced exactly this overflow, the asyncpg insert raised `numeric field overflow`, and the bus listener short-circuited mid-pass with a noisy traceback so every subsequent pool in the same tick was skipped. Mirrored the same `int(...)` + `abs >= 10**36` guard onto the cross-pair branch so a single bad pool can't blow up the whole rebalance. + +### New Features +- **Brave Search API as a first-class web-search backend**: New `SEARCH_BACKEND=brave` option in `Config` plus `BRAVE_SEARCH_API_KEY` env var. Implementation in `core/framework/agent_tools/tools/data._web_search_brave` calls `https://api.search.brave.com/res/v1/web/search` with the subscription-token header and maps Brave's `web.results[]` rows straight into the existing `{title, url, snippet}` shape -- no AI summary layer in the loop, so `SEARCH_MODEL` is correctly ignored for this backend. Wired into the dispatcher in `data.web_search` (falls through to DDG on key-missing or HTTP error, identical to the other backends), into the `,ai websearch` cog (added to `_SEARCH_BACKENDS`, surfaced in `,ai websearch status` with a `BRAVE_SEARCH_API_KEY: set/not set` chip, and shown in the help text + missing-key hint when an admin pins the guild to `brave`), and into `core/framework/ai/auto_repair.probe_brave` so `,ai doctor` probes the Brave endpoint just like DDG/Perplexity. Added to the `websearch` fallback chain at the front (`brave -> ddg -> openrouter -> perplexity -> ollama`) so when an operator drops a Brave key in Railway the doctor failover automatically prefers it over scraping DDG HTML. Clarified the `SEARCH_BACKEND` / `SEARCH_MODEL` env-var docstrings in `config.py` and `core/framework/ai/models.py` so it's explicit which backends consume the model knob (openrouter/perplexity/ollama) and which ignore it (ddg/brave). +- **`$status` -- live provider + data-point health probe**: New top-level `$` command (aliases `$health`, `$diag`) that probes every registered market provider with a quote against a canary symbol and reports back one chip per provider: 🟢 healthy / 🟡 degraded / 🔴 down / ⚪ disabled, plus latency in ms and the last failure reason. Also probes Redis cache reachability, the OpenRouter AI gate (with a tiny `complete()` call so we actually verify the round-trip), and the TradingView UDF bridge when configured. Replaces "is this provider working?" guesswork with a single `$status` invocation. +- **Live TradingView UDF endpoint inside the bot's FastAPI app**: New `api/v2/routers/udf.py` mounts at `/api/v2/udf` and speaks the full TradingView Charting Library Datafeed Specification -- `/config`, `/symbols`, `/search`, `/history`, `/time`. CORS-open, no auth, no demo data: sources real OHLC from the existing market router (Yahoo Finance for equities/ETFs/forex/commodities/indices, CoinGecko for crypto, DexScreener for DEX pairs). External TradingView clients can now pull live cross-asset data from this bot directly. Built-in recursion guard so pointing `TRADINGVIEW_UDF_URL` at the bot's own URL doesn't create an infinite loop (the UDF handler short-circuits the in-router TradingView provider for the duration of its own request). +- **Switchboard adapter rewritten to use the public Crossbar gateway**: No more `@switchboard-xyz/solana.js` SDK requirement, no Solana RPC, no keypair. The new adapter calls `https://crossbar.switchboard.xyz/simulate//` directly via HTTP and returns the medianised oracle value. Operators configure feed hashes per symbol via `SWITCHBOARD_FEEDS={"BTC/USD":"0x...","ETH/USD":"0x..."}` (pulled from `https://ondemand.switchboard.xyz/`). Without any feed hashes the provider stays disabled and the router falls through to Pyth + RedStone (which already cover every major). Replaces the previous stub that always returned `None`. + +### Changes +- **Legacy `$` handlers fully migrated into `cogs/_dollar/legacy/`**: All 12 handler bodies (`_handle_chart`, `_handle_scan`, `_handle_info`, `_handle_global`, `_handle_top`, `_handle_trending`, `_handle_movers`, `_handle_heatmap`, `_handle_fear_greed`, `_handle_dominance`, `_handle_convert`, `_handle_channels`) plus their shared embed builders / formatters (`_fmt_big_usd`, `_fmt_price_usd`, `_summarize_indicators`, `_build_scan_embed`, etc.) moved out of `cogs/realmarket.py` into `cogs/_dollar/legacy/` (one file per handler, helpers in `_shared.py`). The cog shrank from 2104 lines to 423 -- just the `on_message` dispatcher, the channel allowlist, `_resolve_or_error`, and a thin one-line shim per legacy `_handle_*` method so existing callers (the dispatcher itself, `cogs._dollar.market_handler`, `cogs._dollar.legacy_handlers`) keep working unchanged. +- **Discoverability surfaces updated to teach players about the `$` namespace**: `,help` gains a new `realmarket` category (group 2 dropdown, gold border) covering every `$` command with examples, the timeframe catalogue, the AI mode, and the trusted-source guarantee. The `,tour` onboarding deck gets a new "Real Markets -- the `$`-prefix" card right after Sage, and the closing "Where to look next" card now points at `,help realmarket` + `$help`. The `,start` game hub embed gains a "📡 Real Markets" pointer below the game tiles. `$help` itself was rewritten as a multi-field embed with parity content to `,help realmarket` so the two surfaces stay in sync. +- **AI lexicon + channel hints know about the `$` namespace**: `services/ai_lexicon.py` gains a new section #14 (REAL-MARKET NAMESPACE) primed with the eight top-level groups, the timeframe catalogue, the provider stack, and an explicit instruction that when a user asks the AI about real-world markets the AI should route them to `$query` / `$info` / `$compare` rather than reaching for game state. Both the aichannel hint and the bot-channel hint in `services/ai_context.py` get a REAL-MARKET HOOK / REAL-WORLD MARKET ROUTING block telling the model to name-drop the matching `$` command on live-market questions while never conflating the game's simulated tokens with real-world ones. +- **Slash-command surface cut from 29 to 11**: The slash picker is the bot's most prominent discoverability surface and was leaking dozens of game-action commands (`/farm`, `/fish`, `/delve`, `/craft`, `/expedition`, `/inventory`, `/items`, `/db`, `/mastery`, `/me`, `/notify`, `/autolevelup`, `/2fa`, `/apex`, `/war`, `/games`, `/market`, `/fun`, `/calendar`, `/botinfo`). Flipped `with_app_command=False` on every one of those so they remain fully functional as prefix commands (`,farm`, `,fish`, ...) but drop off the slash picker. Final slash surface: `/help` `/tour` `/start` `/today` `/balance` `/profile` `/menu` `/leaderboard` `/inbox` `/report` `/disco` -- 11 discovery-tier commands covering both `,` (game) and `$` (real-market) ecosystems. The `$` namespace contributes zero slash commands by design (prefix-only `on_message` dispatcher). +- **`/disco` becomes a single group instead of four standalone commands**: `disco_facts`, `disco_forget`, `disco_remember`, `disco_listen` were four separate top-level slash commands. Folded them into one `commands.hybrid_group(name="disco")` parent with `forget` / `facts` / `remember` / `listen` subcommands. The legacy prefix names (`,disco_forget` etc.) still work as aliases on each subcommand so existing usage isn't broken. Slash users now see one clean `/disco` entry with autocompleted subcommands instead of four siblings cluttering the picker. + +### New Features +- **`$watch` alerts now fire on Discord via a background worker**: New `services/market/watch_worker.py` polls active `market_watchlist` rows every `MARKET_ALERT_INTERVAL` seconds (default 60), pulls a fresh quote per distinct symbol through the market router, and posts a one-shot alert to the channel the user `$watch add`-ed from (DM fallback if the channel isn't reachable). The worker is owned by the `RealMarket` cog and starts in `cog_load` / stops in `cog_unload`. Each row triggers exactly once -- `triggered_at` is stamped after delivery so re-triggers require the user to re-add the alert. +- **`$chart` and `$info` now resolve equities, ETFs, forex, commodities, and indices**: `_resolve_or_error` tries CoinGecko first (crypto path stays unchanged and fast), then falls through to the cross-asset market router for anything else. Non-crypto matches route into new `cogs/_dollar/chart_handler.handle_chart_router` and `cogs/_dollar/info_handler.handle_info_router` which fetch OHLCV via Yahoo (free, no key) and render via the existing `core.framework.chart.build_chart_png` so the visual style matches `,chart` exactly. `$info MSFT` shows P/E, EPS, 52-week range, exchange + next earnings (Finnhub when keyed, Yahoo otherwise). `$chart SPY 1w` renders an equity candle chart with no crypto data anywhere in sight. +- **Stable import surface for the legacy `$` handlers**: New `cogs/_dollar/legacy_handlers.py` exposes thin async wrappers (`chart`, `scan`, `info`, `global_summary`, `top`, `trending`, `movers`, `heatmap`, `fear_greed`, `dominance`, `convert`, `channels`) that delegate to the live `RealMarket` cog. This gives every legacy handler a canonical `cogs._dollar.legacy_handlers.X` import path so future incremental code moves can happen one handler at a time without touching every caller. The bodies stay in `cogs/realmarket.py` for now -- moving them piecemeal under test is a follow-up, but the import surface is already set up to absorb them. + +- **`$` market namespace becomes a real cross-asset platform**: The `$` commands used to be a thin CoinGecko wrapper (6 timeframes, crypto only). New `services/market/` provider-adapter package fans out across CoinGecko, Yahoo Finance (equities/ETFs/forex/indices/commodities), Finnhub (fundamentals, earnings, news), DexScreener (DEX pairs), Pyth Hermes / RedStone / Switchboard (oracles), CoinGlass / Coinalyze (perp funding + OI + liquidations + long/short), and a self-hosted TradingView UDF feed when configured. A health-aware router picks the right provider per `(asset_class, timeframe, capability)` and silently falls through when one is down or its key is missing -- the bot still serves `$help` and `$chart btc 1d` with zero new API keys configured. +- **Full timeframe catalogue (24 codes from `1s` to `all`)**: New `services/market/timeframes.py` is the single source of truth -- `1s, 5s, 15s, 30s, 1m, 3m, 5m, 15m, 30m, 45m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1mo, 3mo, 6mo, 1y, all`, with per-provider native granularity and a documented fallback chain so anything finer than a provider supports is rejected with a clear hint instead of silently degrading. +- **`$scan SYMBOL TF ai` -- probabilistic AI commentary on the technical scan**: Append `ai` to any `$scan` to get a follow-up embed with an LLM-authored explanation of the detected pattern, conflicting signals, and a 0..1 confidence chip. The model is hard-pinned away from price targets and game references -- every numeric claim is bracketed to a provider, and citations surface through the existing ephemeral Sources button (reuses `cogs/help.py:_SourcesView`). +- **`$query` -- professional AI market Q&A**: A new top-level `$query` command (alias `$q`) lets users ask real-world market questions ("upcoming IPOs", "recent earnings for Firefly Aerospace", "how did ETH move vs BTC in the last 72 hours") and get a sourced, neutral response. Strictly excludes the player profile / net worth / game-state blocks the chat AI normally injects. URLs are sanitised through a gold-standard allowlist (sec.gov, reuters.com, bloomberg.com, pyth.network, finance.yahoo.com, coingecko.com, ...) before the Sources button shows them -- sketchy or out-of-date domains are dropped. +- **`$watch` -- personal watchlist + price alerts**: `$watch add BTC 75000 above` stores a target-price alert; `$watch list` shows your entries; `$watch remove BTC` deletes one. New `market_watchlist` table backs the storage; per-user cap configurable via `MARKET_WATCH_MAX_PER_USER` (default 20). +- **`$compare`, `$oracle`, `$funding`, `$oi`, `$market` umbrella**: `$compare BTC SPY` gives a side-by-side normalised view. `$oracle SOL` returns a Pyth/RedStone/Switchboard medianised quote with confidence interval, divergence flag, and stale-feed warning. `$funding BTC` / `$oi BTC` surface perp funding rate and open interest aggregated across exchanges. `$market ` is the umbrella for the existing market-wide handlers (`fear`, `heatmap`, `gainers`, `losers`, `trending`, `top`, `dom`, `global`, `convert`) -- every legacy alias (`$fear`, `$heatmap`, ...) still works. + +### Changes +- **`$help` becomes the tour-style entry point for the new namespace**: New `cogs/_dollar/help_handler.py` reorganises the help embed around the 8 top-level groups (`$chart` `$info` `$scan` `$market` `$compare` `$watch` `$oracle` `$query`) and surfaces the new timeframe catalogue, the `ai` modifier, and the trusted-sources guarantee. The legacy long-form reference card lives unchanged inside `cogs/realmarket.py` for now and is reachable from `$market help`. +- **Postgres OHLCV cache table**: New `market_ohlcv` table (`(symbol, tf, ts)` primary key) lets `$chart` / `$scan` answer from local storage when Redis is cold or upstream is rate-limited. Population is best-effort by the provider layer -- writes silently no-op when the table isn't there yet. +- **Codebase cleanup pass: dead cogs, ghost modules, and bad-import bugs purged**: Removed the unloaded `cogs/drops.py` (392 lines) -- it duplicated the auto-drop loop and `,airdrop` command that already live in `cogs/faucet.py`, but was never added to the COGS loader, so it just rotted as a ghost cog while faucet served the real surface. Moved `cogs/rate_model.py` out of the cogs/ namespace into `services/rate_model.py` (it has no `setup()`, only the Aave-style utilization math used by bank.py savings/lending rate displays) and rewired the bank.py import. Deleted the deprecated `Database.get_user_full_net_worth()` helper -- nothing has called it since `services.net_worth.compute_net_worth` became the single source of truth, per CLAUDE.md. Autoflake swept 40+ files of unused module-level imports across cogs/, services/, core/framework/, database/, api/. Pyflakes turned up real runtime bugs that are now fixed: `cogs/rugpull.py` was calling `asyncio.create_task` without importing asyncio (would crash on every rugpull win/loss event reaction); `cogs/trade.py` had two `_logging.getLogger(...)` calls that referenced an undefined alias instead of the module's `log`; `cogs/shop.py` shadowed its `math` import to `_math` but still called `math.sqrt` on the level/XP path (would crash on every stone level-up); `cogs/admin.py` used `C_AMBER` without importing it (would crash on the `,admin helpers remove` and `,admin helpers announce_role clear` confirmations); `cogs/security.py` used `C_SUCCESS` without importing it (would crash on the security toggle confirmation); `api/v2/routers/nfts.py` `/nfts/{id}/buy` was inserting an undefined `price` into the sale row instead of `price_raw` and returning the same undefined symbol in its response payload; `api/v2/routers/mining.py` `/mining/buy-rig` was using an undefined `verify["wallet"]` to compute a value that wasn't returned; `api/v2/routers/staking.py` was returning an undefined `penalty` instead of `penalty_human`. Each of these would have been a 500 on the first call -- now they actually work. +- **Codebase cleanup pass: dead cogs, ghost modules, and bad-import bugs purged**: Removed the unloaded `cogs/drops.py` (392 lines) -- it duplicated the auto-drop loop and `,airdrop` command that already live in `cogs/faucet.py`, but was never added to the COGS loader, so it just rotted as a ghost cog while faucet served the real surface. Moved `cogs/rate_model.py` out of the cogs/ namespace into `services/rate_model.py` (it has no `setup()`, only the Aave-style utilization math used by bank.py savings/lending rate displays) and rewired the bank.py import. Deleted the deprecated `Database.get_user_full_net_worth()` helper -- nothing has called it since `services.net_worth.compute_net_worth` became the single source of truth, per CLAUDE.md. Autoflake swept 40+ files of unused module-level imports across cogs/, services/, core/framework/, database/, api/. Pyflakes turned up real runtime bugs that are now fixed: `cogs/rugpull.py` was calling `asyncio.create_task` without importing asyncio (would crash on every rugpull win/loss event reaction); `cogs/trade.py` had two `_logging.getLogger(...)` calls that referenced an undefined alias instead of the module's `log`; `cogs/shop.py` shadowed its `math` import to `_math` but still called `math.sqrt` on the level/XP path (would crash on every stone level-up); `cogs/admin.py` used `C_AMBER` without importing it (would crash on the `,admin helpers remove` and `,admin helpers announce_role clear` confirmations); `cogs/security.py` used `C_SUCCESS` without importing it (would crash on the security toggle confirmation); `api/v2/routers/nfts.py` `/nfts/{id}/buy` was inserting an undefined `price` into the sale row instead of `price_raw` and returning the same undefined symbol in its response payload; `api/v2/routers/mining.py` `/mining/buy-rig` was using an undefined `verify["wallet"]` to compute a value that wasn't returned; `api/v2/routers/staking.py` was returning an undefined `penalty` instead of `penalty_human`. Each of these would have been a 500 on the first call -- now they actually work. + +### New Features +- **Sage Network -- crypto learn-and-earn economy + three educational games**: Brand-new SAGE/EDU earn-only network sitting alongside the Gamba surface. `,pattern` renders a Pillow candlestick chart of one of 17 classical chart patterns (head & shoulders, cup & handle, wedges, flags, triangles, ...) and asks the player to pick the right name from four buttons (15s). `,gauge` renders an indicator card (RSI / MACD / Bollinger / OBV / funding / VWAP / stoch / golden-and-death cross / volume climax) and asks bearish / neutral / bullish (30s -- 2x timer for reading time). `,tknom` renders a synthetic token's supply card (supply, daily mint, burn rate, LP lock, founder share) and asks Inflationary / Deflationary / Stable / Rug Risk (15s). Every correct answer mints 10% SAGE + 90% EDU of the round's USD value (~$0.20 base, +10%/round, capped 4x); a single wrong answer ends the run. Every answer (correct OR wrong) shows the educational explanation for the correct pick so the player learns from each round. Run leaderboards live in `sage_runs`; per-game bests live in `user_sage`. `,sage stake/unstake/claim/cashout` mirrors the Gamba/Fishing economy shape (EDU stake -> SAGE drip at 0.0025 SAGE/EDU/day, SAGE -> USD via burn cashout at oracle minus impact). Surfaces are wired into `,help sage`, `,balances` (📚 Sage dropdown), mastery (new `sage_scholar` track), quests (daily Quick Study, daily Pop Quiz, weekly Chart Reader), and achievements (Apprentice / Scholar / Hot Streak / Chart Wizard / Triathlete). +- **Disco refuses to help mid-Sage-run (and roasts you)**: `,ask`, AI mentions, and AI replies all now check the sage-active lock before responding. If you're mid-quiz, Disco returns one of 10 thematic roasts ("Bold of you to ask the bot for the answer to the educational game", "Use your eyes. They came free with the wallet.", ...) instead of giving you the answer. The whole point of the earn surface is that the EDU has to be earned; if the AI is doing the chart reading for you, the firewall leaks. Lock auto-expires after 5 minutes so a dropped run never permanently mutes the AI. + +### Bug Fixes +- **Bot startup crashed with `CommandAlreadyRegistered: Command 'forget' already registered`**: The `/disco` hybrid_group in `cogs/disco_ai.py` was declared with both `fallback="forget"` AND an explicit `@disco.command(name="forget", ...)` subcommand. `hybrid_group(fallback=...)` auto-creates a slash subcommand of that name mapped to the parent's callback, so the explicit `forget` subcommand collided with it and discord.py refused to load the cog -- which crashed the whole bot at startup (cogs/disco_ai.py is in the mandatory cog list). Dropped `fallback="forget"` so the explicit `disco_forget` method owns the slash `/disco forget` subcommand; the parent `disco` callback still renders the help embed when `,disco` is invoked without a subcommand (prefix), and slash users pick a subcommand from Discord's normal autocomplete. +- **`,pattern` (and `,gauge` / `,tknom`) showed an empty image on the 5th / 6th card**: Every round was sent via `ctx.reply` to the player's original `,pattern` message, which auto-deletes after the guild's `cmd_delete_after` window (typically 30s). Once the original command was gone, `DiscoContext.reply` fell back to `ctx.send` -- but with the SAME `discord.File` whose underlying `BytesIO` had already been streamed (and exhausted) by the failed reply attempt. The retry uploaded zero bytes, so the embed's `attachment://...` URL pointed at an empty PNG and the chart rendered as a broken image. Round 1 still uses `ctx.reply` to acknowledge the command; rounds 2+ and the answer-explanation embed now go straight through `ctx.send`, sidestepping the doomed reply→send fallback and the file-reuse hazard it creates. Matches the followup-send pattern `,chess` already uses for multi-move boards. +- **Disc.Fun dropdown always read "0 DFUN" + hid proto positions**: The 🎢 Disc.Fun category in `,balances` was gated on `disc_fun_value > 0` which only counted active proto positions on the bonding curve, so a user with a non-zero DFUN balance, staked graduated tokens, or pending DFUN yield -- but no active protos -- saw no dropdown at all. When the dropdown DID appear it only listed active protos, never the user's actual DFUN balance from `crypto_holdings`. Rewrote the section to always render when ANY of {DFUN balance, active proto positions, staked positions, pending yield, graduated holdings} is non-zero, and to surface each component on its own labelled field (💵 DFUN balance, 🌱 Staked, 🪙 Pending yield, 🚀 Active proto positions, 🎓 Graduated holdings, Σ Total). +- **Delve arena matched copper-rank low-level players against high-level mages**: Matchmaking only filtered by ELO (±200) with no level guardrail, so a Lv 12 copper player whose ELO clustered at the season floor got fed to Lv 30+ opponents (or synthesised CPU dummies mirroring those stats) who one-shot them every round. Added a level filter to the match-query (±25% of player level, minimum ±3) joined against `user_dungeon.level`, plus a belt-and-braces post-query check that falls through to the synthesised opponent path when the resolved profile's level still exceeds the guardrail. + +### Changes +- **Chart price labels are now easier to read on `,chart`, `$chart`, and `$scan`**: The right-axis price ticks and last-value badge on the rendered chart PNG used Lightweight-Charts' default 11px font, which got cramped on Discord's mobile/embed downscaling. Bumped the shared `charts/template.html` to `layout.fontSize: 17` so every command that flows through `build_chart_png` (game-token chart, real-crypto chart, scan alert image) renders price labels that are legible without zooming into the image. Also shrank the in-PNG header (pair name 22 -> 15px, OHLC stats 13 -> 11px, TF chip 12 -> 10px) so the bigger price axis doesn't crowd the candle area. +- **`$chart` and `$scan` no longer brand the chart PNG as "Simulated Market"**: The shared chart template hard-coded "Discoin · Simulated Market" in the footer, which was correct for `,chart` (game-token simulated candles) but wrong on the live CoinGecko-backed real-market charts. Added a `live` flag to `build_chart_png` that the realmarket cog passes for both `$chart` and `$scan`; the template now renders "Discoin" only on live charts and keeps the "Simulated Market" label on game-token charts. +- **Forage / Beachcomb / Scavenge gains now scale with the player's level in that game**: `,farm forage`, `,fish beachcomb`, and `,delve scavenge` were paying flat ranges from config -- a Lv 40 forager pulled the same handful of HRV as a fresh Lv 1. All three now multiply HRV/LURE/RUNE (and SEED/REEL/ORE) credits by `level_payout_mult(level)` (1.0x at Lv 1, ~1.49x at Lv 50) using the same shape farming/fishing already use on the main loop. Added the matching `level_payout_mult` helper to `dungeon_config.py`. + +### AI Infrastructure +- **AI replies now have a 90s hard cap and an elapsed counter in the placeholder**: The outer `wait_for` budget for a single `,ask` / mention / reply was 180s (240s with image), which on a slow cloud Ollama model meant the placeholder could sit on `_thinking..._` for three minutes before surfacing "AI didn't respond". New `AI_REPLY_TIMEOUT_S` env var (default 90s, +30s automatically when an image is attached) caps a single turn far more aggressively, and `ChatStatusRenderer._phase_text` now appends the elapsed seconds (`thinking... (12s)`) so the user can see the request is alive and how far along it is. +- **AI chat now has a separate `CHAT_BACKEND` for casual replies**: The "is it alive?" / "what's up" path used to share `TOOLS_BACKEND` with the agent tool loop, so an operator who set `TOOLS_BACKEND=ollama` for cheap tool calls also got 60-150s casual replies on whatever slow Ollama model they configured (e.g. `gemma4:31b-cloud` at ~60 tok/s). New `CHAT_BACKEND` env var routes casual chat independently -- set `CHAT_BACKEND=openrouter` to get 3-5s replies from `OPENROUTER_MODEL` for chat while the tool loop continues on Ollama. Leaving `CHAT_BACKEND` blank keeps the legacy single-backend behaviour. +- **Killed the cross-provider rescue chain that fanned one chat reply into three model calls**: Every casual `,ask` was running up to three sequential model calls -- Ollama with the guild-override model, Ollama with the env default, then OpenRouter as a "rescue" -- whenever the first one returned empty content. On a slow cloud-Ollama deployment this added 60-200s of pure dead weight per reply AND silently billed OpenRouter on every transient Ollama burp, which is the exact behaviour operators picked Ollama to escape. Removed the same-backend env-default retry and gated the cross-provider rescue behind a new `AI_CROSS_PROVIDER_RESCUE` env var (default off). A casual chat is now exactly one call to one backend. +- **Ollama requests now ship `keep_alive` so the model stays warm between turns**: Hosted Ollama unloads a model after ~30s of idle, so the next request paid a 5-15s cold reload before the first token. Added `OLLAMA_KEEP_ALIVE` env var (default `10m`) wired onto every Ollama request payload (chat completion, streaming, tool calls, vision OpenAI-compat AND native /api/chat). Cold-reload spikes between bursts of AI traffic are gone. +- **Trimmed the Ollama retry storm from 3 attempts to 2**: The third retry on 429/5xx multiplied a 60-90s wait into 180-270s, which routinely blew the outer 180s `wait_for` budget and surfaced "AI didn't respond" even when the very first response was on its way. One retry on transient failures is enough. +- **AI replies no longer silently bill OpenRouter when TOOLS_BACKEND=ollama**: Every chat reply was firing 1-2 extra OpenRouter completions (memory refresh, passive trait extraction, ambient crypto reply, tool suggestions) because the background side-effect path went directly through `core.framework.ai.complete` -- which is hardcoded to OpenRouter and never consulted `TOOLS_BACKEND`. Operators on an Ollama-only deployment saw 3 different models per chat turn in OpenRouter logs (the configured `OPENROUTER_MODEL` plus whatever per-guild override they'd ever set). Added `core.framework.ai.complete_default`, a backend-aware non-streaming helper that routes to Ollama (via `TOOLS_MODEL`) when `TOOLS_BACKEND=ollama` and falls back to OpenRouter only when Ollama returns empty and an API key is configured. Wired every per-reply background caller (`_update_user_memory`, `run_post_message_tasks`, `_maybe_suggest_tool`, `handle_ai_ambient`, batch memory refresh, `_run_ai_chat` fallbacks) through the helper so the operator's chosen backend handles all auxiliary turns too. +- **Stale per-guild AI model overrides no longer bounce the tool loop to OpenRouter**: `_resolve_tools_pick` previously had a guild-wins precedence, so a leftover `,ai model set tools openrouter:google/gemma-3n-e4b` row would force every tool-calling round onto OpenRouter even on a deployment that explicitly set `TOOLS_BACKEND=ollama`. Flipped to env-wins (consistent with `core.framework.ai.resolve_model`): the env backend now picks the provider, and a guild row is only honoured when its provider matches the env backend. Same fix applied to `_resolve_domain_model` for the risk/automation/defi/economy_sim final-pass routing. Migration `0267_clear_ai_model_overrides.sql` wipes the legacy rows so `,ai model show` returns a clean slate; admins who genuinely want a per-category override can re-set it. + +--- + +## [main] — 2026-05-14 + +### Discord Bot +- **Auto-delete user's $-command message to match ,chart behaviour**: The bot's reply auto-delete (reply_delete_after) already runs through; DiscoContext.reply(), so the chart embed / info card already disappear; on the guild-configured schedule. The user-message auto-delete (`f124ad8b`) +- **$chart: sub-hourly timeframes, strict tf parsing, $channels allowlist, richer $help**: Three player-visible fixes plus a new admin surface:; 1. $chart now supports 5m / 15m / 30m as well as 1h / 4h / 1d. The; sub-hourly tier comes from CoinGecko's /market_chart endpoint (5-min (`9ccbebc7`) +- **Restore ,chart + bring $chart to full ,chart feature parity**: The previous pass retired the bare ',chart' alias so the '$' namespace; could own the 'chart' name. Bad call: ',chart' should remain a; first-class shortcut for the game chart, and '$chart' should be its own (`62e8d517`) +- **Add $-prefixed real-crypto chart + info commands (live CoinGecko data)**: Splits "market intel" into two surfaces alongside the existing simulated; game chart: $chart renders a live candlestick PNG with the same 20+; technical indicators, $info packs price deltas / 24h H-L-vol / market (`5630a557`) +- **Fix delve arena duel cooldown bug + redesign arena ASCII + monarch embeds**: Bug fix: `,delve arena duel @user` was hitting an eternal 60s cooldown; for any challenger with no prior invite row in the season. The cooldown; query returned NULL when no row matched, and `int(busy_s or 0)` coerced (`5f8c131a`) +- **Queen of Rugs role, crown-discount + ,rugdefend active defense**: Add a "Queen of Rugs" path to the rug-pull minigame so female players who; take the throne get a gendered role instead of the King of Rugs role.; Only one monarch role can be held across both King and Queen at a time; (`8b28b580`) +- **Delve arena: mobile-readable ASCII frame; charts: 20+ indicators**: Arena ASCII frame (services/delve_arena_render.py); - Was 46 cols wide, which folded both borders onto a second line on; iPhone portrait + cramped every stat into half-width side-by-side (`bd622af3`) +- **Buddy battle + arena as help-root groups; help/tour/AI context refresh**: * `,buddy battle` is now a group; the bare invocation shows a help; embed explaining the surface, with `,buddy battle fight @rival; [amount]` running the actual PvP duel (aliases challenge / duel). (`6c015f4b`) +- **Drop the grid tile block from the field embed**: The mines-style [W]/[c]/[.] grid stacked under the weather frame; looked redundant against the verbose per-plot list below it and; ate vertical space without adding signal. Reverting to the single (`2f3d42d4`) +- **Delve arena: drop conflict-prone subcommand aliases**: Boot was crashing with CommandRegistrationError because the `rank` alias; got reused on two arena subcommands (`fight` and `profile`), and a few; other aliases collided with existing top-level commands. Stripping (`2f4ab985`) +- **Core systems overhaul: arena PvP + delve mob ASCII + farming/fishing expansion + wecco fix**: * Delve arena PvP (new): per-season ELO ladder (Copper / Silver / Gold / Rune),; ranked async matchmaking and a live `,delve arena duel @user` mode with an; optional `unranked` flag. Reuses each player's existing delve combat profile (`9520f15e`) + +### Framework +- **Ai chat: retry + cross-provider fallback on empty bridge responses**: The streaming bridge's no-schemas fast-path (casual chat with no tool; schemas in scope) had no fallback at all -- when hosted Ollama Cloud; returned HTTP 200 with an empty body (safety filter, transient blip, (`d26abd44`) + +### Services +- **Portrait + arena render polish**: * Draclet (bat) wings now attach cleanly: inner anchor sits inside the; body silhouette, scalloped outer edge, body painted first so the; wings overlap into the torso instead of floating beside it. Belly (`965bf7fd`) + +--- + +## [main] -- 2026-05-14 + +### New Features +- **Market-wide `$` tools (`$global` / `$top` / `$trending` / `$gainers` / `$losers` / `$heatmap` / `$fear` / `$dom` / `$convert`)**: Expanded the `$`-prefixed namespace from per-coin lookups (`$chart`, `$info`, `$scan`) into a full market-tools surface so players can scan the whole market at a glance. `$global` (`$g`/`$total`/`$overview`) shows total crypto market cap, 24h volume, BTC/ETH dominance, active-coin count, and 24h cap change. `$top [N]` (`$t`/`$markets`) renders the top N coins by market cap with prices, 24h %, and caps, colour-flagged per coin. `$trending` (`$tr`) shows the 7-15 most-searched coins on CoinGecko in the last 24h. `$gainers` / `$losers [N]` re-sort the top-250 by 24h % so the filter excludes meme-tier +9999% noise. `$heatmap [N]` (`$hm`/`$hmap`) renders a colour-coded grid (🟩 ≥+5%, 🟢 >0%, ⚫ flat, 🔴 <0%, 🟥 ≤-5%) plus an up/down/avg-move summary chip. `$fear` (`$fg`/`$greed`) pulls the alternative.me Crypto Fear & Greed Index (0-100) with a 20-tick bar and yesterday/week/month deltas. `$dom` (`$dominance`) shows each top coin's market-cap share as a 24-tick horizontal bar. `$convert ` (`$conv`) converts amounts between any two coins (or coin <-> USD). All new commands are Redis-cached (120s markets/globals/tickers, 300s trending, 600s F&G) and respect the same `$channels` allowlist as the rest of the surface. +- **Useful whale data in `$info`**: The whale field used to just print a contract address (e.g. "Contract on ethereum -- `0xa0b86...c2eb48`") which players found useless. It now shows what people actually want to see -- **USD movement amounts**: the top 5 exchange venues by 24h USD volume on the asset (e.g. "💱 **Binance** `BTC/USDT` · $4.27B · 38.4% of 24h vol"), aggregated across pairs per exchange so a single CEX with multiple stablecoin pairs doesn't take all five slots. Below the venues, a turnover chip shows the volume / market-cap ratio classified as 🌶️ high / 🔥 active / 🟢 healthy / 🟡 quiet so high-velocity tokens are obvious at a glance. +- **`$scan` / `$pattern` -- heuristic chart-pattern detector**: New `$`-prefix command runs a heuristic detector over live OHLC and emits a styled alert embed naming the detected pattern, the timeframe, support/resistance touch counts, status (Forming / Confirmed / Breakout / Breakdown), confidence score, and a chart screenshot. Recognises bear/bull flags, double tops/bottoms, head & shoulders (and inverse), ascending/descending/symmetrical triangles, and rising/falling wedges. Default timeframe is 30m; aliases `$s`, `$pattern`, `$p`. Embed colour follows pattern bias (bull / bear / neutral) and the body always carries a "DYOR | Not financial advice." disclaimer. The chart underneath the alert now has the detected support / resistance / neckline / flagpole drawn on it -- themed line series (resistance red, support green, neckline dashed gold, flagpole purple) plus arrow markers on the pattern's key pivots (pole start, flag end, double-top peaks, H&S shoulders / head, etc.). +- **Pattern lore + market context in `$scan`**: Every detected pattern now ships with a "📖 What this means" field that explains what the pattern is, what historically follows it, and what each status (Forming / Confirmed / Breakout / Breakdown) implies for the next move. A separate "📈 Market context" field summarises the scanned window: total move %, range %, average per-bar volatility, total volume, average volume per bar, and a late-window-vs-early-window volume-trend chip (suppressed on timeframes where CoinGecko's free tier doesn't expose volume). +- **`$chart` opt-in pattern auto-tag**: Pass `scan` (or `pattern`) as a flag on `$chart` to add a one-line pattern tag to the chart embed's description -- e.g. `$chart BTC 1h scan` will append "🚩 Bear Flag spotted (forming, 87% conf) -- `$scan BTC 1h` for details" inline. Off by default so the existing chart surface stays focused on the chart itself. +- **Real-crypto market commands (`$chart` / `$info`)**: New `$`-prefixed namespace renders live charts and a packed market snapshot for any CoinGecko-listed coin (BTC, ETH, SOL, ...) -- fully separate from `,chart` and the simulated game tokens. `$chart SYMBOL [tf] [indicators/flags...]` ships at **full feature parity with `,chart`**: every indicator (rsi/macd/bb/stoch/atr/adx/supertrend/psar/ichimoku/donchian/keltner/pivots/roc/wpr/cci/mfi/mom/obv/vwap/vol/trend/all/ema+N/sma+N/wma+N), every layout flag (wide/tall/light/dark/minimal/line/area/candles/heikinashi/bars/log), plus `compare:SYM` overlays (normalised to 100) and `in:SYM` re-quoting against another live coin. `$info SYMBOL` returns one embed with 1h/24h/7d/30d deltas, 24h high/low/volume, market cap + FDV, supply, ATH/ATL with dates, an RSI/MACD/EMA/BB/ADX/ATR readout, best-effort whale data, and three recent headlines. Embeds are gold-bordered and prefixed `[LIVE]` so live-market data can't be mistaken for the simulated game tokens. Responses are Redis-cached (60s OHLC / overview, 300s news, 1d symbol-resolution) and retry with exponential backoff on 429/5xx. +- **Sub-hourly real-market timeframes (`5m` / `15m` / `30m`)**: `$chart` now supports `5m`, `15m`, `30m`, `1h`, `4h`, `1d`. For the sub-hourly tier we pull the 5-minute price stream from CoinGecko's `/market_chart` endpoint and synthesise OHLC candles by bucketing -- the existing indicator engine works against them unchanged. Anything sub-5-min isn't available on the free tier and is rejected with a clear error rather than silently mislabelling the chart. +- **`$channels` admin command (real-market allowlist)**: New `$channels add|remove|list|reset` lets server admins enable `$chart` / `$info` in specific channels **without** also adding those channels to `bot_channels`. This way you can spin up a `#crypto-talk` channel that runs the real-market commands but stays free of the broader game-command surface. Requires the **Manage Server** permission. The effective allowlist is the union of `bot_channels` and the new `realmarket_channels` -- both empty means $-commands run everywhere (unchanged default). +- **Richer `$help` page**: `$help` now renders a categorised reference -- timeframes, every trend overlay, every oscillator, every layout flag, the `compare:` / `in:` overlay syntax, the `$info` snapshot description, the new `$channels` admin commands, and worked examples. + +### Bug Fixes +- **`$chart` mislabelled the timeframe when the user typed an unsupported value**: `$chart btc 15m heikinashi` rendered a 1-hour chart but stuck "15M" into the footer chips because the unsupported timeframe was silently swept into the indicator-flag list. The tail-parser now detects any `\d+[mhdw]`-shaped second token and rejects it with a friendly error listing the supported timeframes when it isn't one of them. +- **`$` commands ignored the guild's `cmd_delete_after` auto-delete window**: the user's `$chart` / `$info` / `$channels` message lingered in chat even when `,chart` and other `,`-commands got cleaned up, because the listener that handles `$` bypasses discord.py's command dispatch (where the framework wires its `on_command` auto-delete). The cog now schedules the user-message deletion itself, mirroring the framework hook one-for-one (including the de-dupe set the `on_message_delete` audit handler reads). + +### Changes +- **Sub-cent price formatting in `$info`**: Memecoins like SHIB rendered as `$0.00` because `fmt_usd` is locked to 2 decimals. A new `_fmt_price_usd` helper adapts decimals to magnitude (2 / 4 / 6 / 8) so sub-dollar tokens actually show a price across the price chip, the 24h H/L chips, and the ATH/ATL chips. +- **`$help` covers the new market-tools surface**: `$help` now documents the new `$global` / `$top` / `$trending` / `$gainers` / `$losers` / `$heatmap` / `$fear` / `$dom` / `$convert` commands alongside the existing chart / info / scan / channels surface, and the intro no longer claims only `$chart` and `$info` respect the `$channels` allowlist (every `$` command does). +- **Chart math single source of truth**: The 600+ lines of indicator math, candle aggregation, layout-flag parsing, and headless-browser render pipeline that lived inline in `cogs/trade.py` now live in `core/framework/chart.py`. Both `,chart` (simulated) and `$chart` (live) import from there, so any indicator fix or layout tweak ships to both surfaces in one place. + +### Database +- **Migration 0266**: Adds `realmarket_channels TEXT NOT NULL DEFAULT ''` to `guild_settings` -- the per-guild allowlist managed by `$channels`. The docker-entrypoint hotfix block applies the column on existing DBs too. + +## [main] -- 2026-05-13 + +### New Features +- **Queen of Rugs role + gender detection**: Winning the rug pull now grants the **Queen of Rugs** role for female players and the **King of Rugs** role for males (single monarch at a time across both). Gender is inferred from the winner's display name / username with an AI fallback, and the player can pin it manually with `,ruggender male|female|clear`. The new `RUGPULL_QUEEN_ROLE` env var configures the Queen role id, mirroring the existing `RUGPULL_ROLE` for the King. +- **Crown discount on rug attempts**: While the throne is held, every rug-pull tier costs **50%** less by default. The discount grows linearly the longer the monarch has held the crown, reaching **85%** off after 48 hours -- long reigns are cheaper to topple, which keeps the game from stalling on whales who sit on the crown forever. +- **Monarch active defense (`,rugdefend`)**: King/Queen can now burn USD from their wallet to buy a temporary success-chance debuff applied to every challenger, similar to `,defend` in the exploit minigame. Each dollar spent buys 0.04% defense, capped at +40%, lasting two hours, with a one-hour cooldown enforced on the DB clock. +- **Delve arena ASCII frame -- mobile-readable redesign, v2 (top-down, no mirroring, no sprite noise)**: The first pass kept a mirrored card (P2 stat block bottom-up under the sprite divider) which players reported was still unreadable. Rebuilt again from scratch: 36-column card with both fighter sections laid out **identically top-down** behind explicit `+--- P1 ---+` / `+--- P2 ---+` labelled dividers, ASCII sprites dropped entirely (the procedural class art was noise), and HP gets a full-width row per fighter with bar + numbers + percent all on one line (bar auto-shrinks for 4-digit max-HP so the closing bracket and pct never get eaten). Cooldown row only renders when there's actually a cooldown to show, multi-cooldown lists no longer push stats off the card, and action banners now use a single ASCII arrow (`STRIKE -> BRACE`) instead of `vs` so the per-round action read is unambiguous. Terminal frames carry the win / draw / KO callout in a dedicated bottom band. +- **King / Queen of Rugs overview embed now reflects the Queen update**: `,games rugpull` was still titled "👑 King of Rugs" with King-only flavor text and no mention of Queen role, crown discount, `,rugdefend`, or `,ruggender` -- it predated the dual-monarch shipment. Title is now "King / Queen of Rugs", body explains the gender-driven title pick and the crown discount, and the action grid surfaces all four new commands (`,rugdefend`, `,ruggender`) alongside the existing ones. `,help economy` rugpull copy and `,lb rugpull` description got the same monarch-neutral pass. +- **Charts overhauled -- 20+ indicators, comparisons, conversions, themes**: `,chart` now ships a proper TA workbench. New oscillators: Stochastic, ATR, ADX (with +DI/-DI), CCI, Williams %R, MFI, ROC, momentum, OBV. New overlays: VWAP, Donchian channels, Keltner channels, SuperTrend (with direction), Parabolic SAR, Ichimoku Kinko Hyo (tenkan/kijun/senkou A+B with forward 26-bar cloud displacement/chikou span), classic floor-trader pivots (S3-R3). Period-tunable via numeric suffix (`rsi21`, `atr10`, `cci14`). Convenience bundles: `trend` (EMA 20/50/200) and `all` (sensible default loadout). Symbol comparison via `compare:BTC compare:ETH` overlays both series normalised to 100 in a stacked panel. Quote conversion via `in:ETH` re-prices the full OHLC stream in another token. Layout flags: `wide` (1800x800), `tall` (1200x1100), `minimal` (no chrome), `light`/`dark` themes, chart-type toggles `line`/`area`/`bars`/`heikinashi`/`candles`, and `log` price scale. Header gets an inline OHLC + percent stats strip with bull/bear colouring; footer carries a coloured-dot legend chip row showing which series are live. +- **Delve arena PvP**: Brand-new ranked PvP and live duel system that uses each player's existing delve combat profile (class, abilities, weapon, armor, relics, allocs). Ranked async fights climb a Copper / Silver / Gold / Rune ELO ladder with per-rank ore + RUNE rewards; `,delve arena duel @user` launches a live, interactive turn-by-turn battle (with an optional `unranked` flag for friendly fights). Per-guild seasonal leaderboards, streak tracking, and flawless-win bonuses all wired in. +- **Delve mob battles use a clean ASCII frame, not buddy art**: Mob fights (goblins, skeletons, slimes, bosses) now render as a proper monospace battle scene inside the embed instead of borrowing the buddy portrait PNG renderer. Wild-buddy encounters during a delve keep the buddy battle PNG; mobs get their own per-tier ASCII art so a slime no longer reads as a procedural blob. +- **Farming overhaul -- tools, perks, harvest combos, seasons**: Adds hand tools (hoe, watering can, sickle, scarecrow) at three tiers each, a 10-perk farmer tree unlocked at level milestones, a 6-step harvest combo bonus, seasonal yield enforcement (in-season +15%, off-season -40%), four new crops (saffron, moon grape, sunheart, mooncress), two new boss pests (Locust King, Crop Wraith), three new recipes, and two new weather events (Hailstorm, Gold Rain). The field embed now ships with a mines-style compact grid so plot states are visible at a glance. +- **Fishing overhaul -- sea monsters, augments, depth, tournaments**: Adds seven sea monsters with their own ASCII boss fight, three rod augment slots (Line, Lure, Reel) at five tiers each, zone-locked legendary fish, per-zone depth + current modifiers, and weekly seasonal tournaments with four themes (Biggest Catch / Legendary Hunt / Heavy Hauler / Variety Run) and a top-10 LURE payout pool. +- **Arena achievements (20+)**: Tier-up badges (Copper / Silver / Gold / Rune), streak badges, Flawless Victory, volume tiers, plus farming combo badges, monster slayer, augmented, legendary diver, and tournament champion badges. + +### Bug Fixes +- **Delve arena duel: eternal 60s cooldown when no prior invite existed**: `,delve arena duel @user` was raising a "Duel invite cooldown: 60s left" error forever for any challenger who'd never sent an invite. The cooldown query `SELECT EXTRACT(EPOCH FROM (NOW() - created_at)) ... LIMIT 1` returns NULL when no row matches, and the Python `int(busy_s or 0)` coerced that to 0 (i.e. "0 seconds elapsed"), tripping the 60s gate every call. Now treats a missing row as "no recent invite -> cooldown elapsed" so first-time and dormant challengers can actually fire off a duel. +- **Ai chat: no more empty replies when the tools model returns nothing**: The streaming + non-streaming bridges now retry the env-default model when a per-guild override returns empty content on the no-schemas fast-path, and fall over to OpenRouter when hosted Ollama Cloud serves an HTTP 200 with an empty body. Casual @-mentions and ,ask calls no longer surface a bare "AI didn't respond" card just because the configured cloud model burped on a single turn. +- **Wecco portrait now reads as a duck**: The procedural wecco draw was five overlapping cloud puffs that came across as a blob. Replaced with a proper egg body + tucked wings + paddle feet + a flat orange duck bill so the silhouette is unmistakable in both buddy panels and battle scenes. + +### Database +- **Migration 0264**: Adds `tools`, `perks`, `combo_step`, `best_combo_step`, `scarecrow_count` to `user_farming`; `augments`, `monsters_defeated`, `treasures_pulled` to `user_fishing`; new tables `fishing_tournaments`, `fishing_tournament_entries`, `user_delve_arena`, `delve_arena_matches`, `delve_arena_duel_invites`, `delve_arena_seasons`. +- **Migration 0265**: Adds `rugpull_gender` table (cached gender per `(user_id, guild_id)`, manual or auto source) and `active_defense_until` / `active_defense_bonus` / `defense_last_used_at` columns on `rugpull_king` so the new `,rugdefend` command has somewhere to store its timed buff and cooldown. + +### Services +- **Ai chat: per-user flavor table -- loosen Wecco, add 8 regulars + Elma**: Restructured services/ai_easter_eggs.py from a one-off Wecco; peak-cringe full-voice override into a unified _FLAVORS table that; contributes a SHORT 2-4 sentence flavor block when one of the known (`56ebc33a`) +- **Buddy portraits: connect floating heads + cute ":3" cat mouth + cleaner cat ASCII**: Floating-head fix:; The fox + cat PNGs were painting the head ellipse with a 5-10% gap; above the body, so the silhouette read as a head hovering above a (`15456bb0`) +- **New cat species -- pastel-grey, big head, perky ears, cute as fuck**: SPECIES["cat"]:; - emoji 🐈, weight 14, trade lane (Sly Hunter rebate); - ability_key "first_strike" / ability_name "Pounce" -- first attack (`335943cc`) +- **Buddy arena: capture fallback, list_storage robust, thornling legs, drop muzzle trapezoid**: Captured bosses missing from storage:; The capture INSERT referenced cc_buddies.boss_zone_id, which only; exists after migration 0263 lands. On a DB where the migration hadn't (`dfb40a66`) +- **Map battle parity with arena: BUD + BBT rewards + rich intro embed**: Two requests rolled together:; 1. ",buddy map battle" never actually paid anything -- on_zone_cleared; computed reward_usd, returned it on ClearResult, and the view (`2a2086e6`) +- **Per-species buddy silhouettes; fix mirrored text in battle scene**: Two compounding problems with the battle-scene portraits:; 1. The right-hand fighter is flipped with FLIP_LEFT_RIGHT so the two; buddies face each other. The previous renderer painted "Lv 7" and (`e8a1ab52`) +- **Fix ,buddy map battle crash: cc_buddies column is is_active not active**: The arena view's _active_buddy() helper queried `active = TRUE` on; cc_buddies, but the column has been `is_active` since migration 0112.; Every ,buddy map travel / battle / boss / tourney pre-check therefore (`81f89ed6`) + +### Discord Bot +- **Ai chat: Continue button for truncated replies**: When the model hits its max_tokens cap mid-thought (finish_reason=; "length") OR the rendered body overflows Discord's 2000-char message; cap, the reply view now ships with a third button -- Continue -- (`6f681e04`) +- **Buddy battles + delve shop: defer collision, special label, pack_size, qty buy**: Buddy arena Special freeze (InteractionResponded):; _handle_action defers up-front for the per-move bursts; _finalize then; called interaction.response.defer() AGAIN, which discord.py raises (`ab9ab854`) +- **Buddy battles: arena victory screen + rarity/type pill on every scene**: Fix 1: ,buddy arena victory now shows a proper final screen.; _BuddyArenaView._render_final_embed was returning just an embed with; no battle scene PNG attachment, so winning the arena dumped the (`f9ea9769`) +- **Buddy arena: wire ,buddy arena view onto the unified battle scene + bursts**: The legacy Buddy Network arena view (_BuddyArenaView in cogs/buddy.py); was the last buddy-battle entry point still painting the old text-only; card embed. Updated to ship the same Pokemon-Stadium scene PNG + (`58aedf29`) +- **Buddy battles: per-move animation bursts wired into fishing, delve, farming**: New shared helper ``services.buddy_battle_scene.play_battle_action_burst``; paints the four-frame attack-burst overlay (Strike yellow slash,; Special blue energy ring, Risky orange motion blur, Brace green shield (`5ec8c1c5`) +- **Buddy battle scene PNG: tied into every battle entry point across the game**: New shared adapter ``services.buddy_battle_scene.fighters_to_scene_state``; takes any two Fighter-shaped objects (Fighter / _DelveFighter / pest; stub / etc) and produces the dict shape ``render_battle_frame`` (`10e83e54`) +- **Buddy battles: PvP/wild bursts + delve scene PNG + map layout fixes**: Map layout:; - Legend overlapped the Whispering Forest header in the old layout; -- moved to a two-row horizontal banner at y=120/148, well above (`12175c34`) +- **Buddy battles: PvP + wild fights now use the same battle scene PNG**: Visual unification first pass. `,buddy battle` (PvP) and the wild; buddy fights inside delves were previously a card() embed with a; text HP bar -- a completely different look from `,buddy map battle` (`20f7c426`) +- **Buddy arena: fix map band overlap + per-boss ASCII frames**: Map layout fix:; Previous canvas had Caldera Ridge and Tideway Coast bands overlapping; in the lower-right quadrant, so Tide Amphitheatre and Champion Hall (`d75091b5`) +- **Buddy arena: bidirectional travel, boss-zone integrity, capture, specials, expansion regions**: Phase 1 -- critical bug fixes:; - Bidirectional zone graph: every neighbour edge auto-mirrors at module; load so Grassy Meadow et al are no longer one-way dead-ends. (`e8e8b13b`) +- **Ai chat: root-cause fix for missing footer/buttons + cut-off replies**: Going deep on why the previous "retry harder" patches didn't actually; fix it. Measured the renderer's edit rate against Discord's; documented 5 edits/5s/channel limit: (`ed5b37ba`) +- **Ai chat: bulletproof final placeholder edit + force-paint on done**: User saw: replies cut off mid-sentence ("Oh, and you", "losing their"),; no -# footer with model/tokens/elapsed, no Regenerate/Try-harder; buttons. Root cause was the renderer's single finalize edit silently (`08040732`) +- **Ai chat: drop per-user serialization + retry finalize on transient errors**: The original chat queue serialized one user's own requests so back-to-; back mentions stacked up behind each other. On a single-user deployment; that produced "queued (#1)" placeholders sitting forever while the (`188e5e10`) +- **Help.py: fix missing regen buttons + flip default queue caps to favor Ollama**: The regenerate / try-harder buttons were silently dropping on the; ,ask / reply / @mention paths. Two separate placeholder.edit() calls; were racing: ChatStatusRenderer.finalize landed first with view=None (`3bdb11a7`) +- **Help.py: attach regenerate view on @mention path**: handle_ai_mention was the third AI entry point but I only wired the; new regenerate / try-harder view onto ,ask and reply-to-bot. Users; who @-mention the bot got the response but no buttons. (`ab6b4a75`) +- **AI chat: per-user queue, regenerate buttons, richer UX, passive learning**: Adds a per-backend chat queue with per-user serialization (core/framework/ai/; queue.py) and live queue-position feedback so the placeholder shows; "queued (#3)" instead of sitting silent until a timeout fires. Replaces (`8e8aa468`) +- **Map/tournament battles now use buddy spells, snappier animation, fix fox nose**: Closes the gap between PvP and PvE battles: zone, boss, and tournament; fights expose the same Strike / Special / Brace / Risky action bar the; PvP duel uses, with Special labelled with the buddy's actual species (`8b342b1a`) +- **Drop Pillow portrait from ,buddy panel; keep ASCII frame only**: Per player request: the main ,buddy embed now ships without the; buddy_portrait.png upload. The in-embed ASCII frame in the embed; description is the only visual on the panel, which keeps the panel (`a33e7456`) + +### Database +- **Buddy arena: boss-tamed buddies are now visually + mechanically unique**: Captured bosses are no longer just high-level species clones. Each; boss zone has a BOSS_VARIANTS entry that drives:; - Unique display name: Meadow King, Granite Champion, Tideborn (`eb8d650e`) + +### Tests +- **Accept new user_id / on_queue_update kwargs in agent_tools mocks**: The chat queue plumbing added user_id and on_queue_update kwargs to; _dispatch_tool_call() and complete(), which the streaming bridge now; threads through every call. The test monkeypatches had stricter (`5331066d`) + +### API Changes +- **Replace wealth tax/equalizer with rank-based Wealth Bottleneck**: Drops every legacy system that drained non-stable holdings from rich; players (the daily wealth_equalizer cycle, CWE per-tx tax + Gini PI; controller + streaming UBI ticks, and the game-token burn phase) and (`997fda1a`) + +--- + +## [main] -- 2026-05-12 + +### New Features +- **Buddy arena: Forest + Volcano regions and four special locations**: Adds 12 new map nodes to `,buddy map`. Forest region (Whisper Path, Fern Hollow, Thorn Thicket, Druid Circle boss) extends Plains/Stone toward the deep woods; Volcano region (Volcano Gate, Ember Steppes, Lava Tube, Magma Caldera boss) sits between the woods and Tide. Four special locations -- Mossy Market (item shop in BUD), Ash Springs (heal roster + free Cure Balm, 1h CD), Smith's Camp (free random consumable, 24h CD), Caravan Clearing (3 rotating offers every 6h) -- give players non-combat reasons to leave the regional grind. Powered by `services/buddy_arena_specials.py`; spend via `,buddy map buy ` / `,buddy map trade `; the visit/heal/dig/trader aliases share the same `,buddy map visit` entry point. +- **Buddy arena: capture wild buddies + rare boss captures**: Restores the Capture button in `,buddy map battle`. Wild opponents become catchable at <=20% HP (base 35% chance + low-HP bonus, up to 85% with luck); region bosses become catchable at <=5% HP (base 3%, hard 15% ceiling, one-shot per zone forever). Mastery `luck.rare_catch` boosts both chances. Captured buddies land in `,buddy storage`; bosses keep their level + tier 3 rarity. Implemented in new `services/buddy_capture.py` so the dungeon delve capture flow stays untouched.- **New buddy species: cat**: A fluffy pastel-grey cat with pink heart nose, perky pink-inner ears, three whiskers per cheek, and a curling tail. Hatches with the **Pounce** ability (first attack of the battle is a guaranteed crit), levels into Evasive (Lv 15) and Killing Blow (Lv 30). Spawns wild in Plains Gate / Grassy Meadow / Windmill Lane / Whisper Path / Fern Hollow / Moonlit Pool. Cute as fuck. +- **Boss-tamed buddies are visually unique + bring named abilities**: A captured boss is no longer just a high-level same-species wolf in your roster. Each boss zone has a `BOSS_VARIANTS` entry that overrides three things at insert + render time: the display name (Meadow King / Granite Champion / Tideborn Sovereign / Bramble Druid / Caldera Tyrant), the in-battle ability (Royal Fury / Bulwark / Tide Decree / Verdant Renewal / Forge Roar), and a portrait overlay (gold crown, iron horned helm, coral trident-crown, antler crown, flame mane) painted on top of the species silhouette plus a small star badge in the corner. Tracked via new `cc_buddies.boss_zone_id` column (migration 0263) so renaming the buddy doesn't wipe the cosmetic, and the battle engine's `Fighter.from_rows` reads the variant's ability key/name when that column is set. The boss intro embed surfaces the title ("King of the Verdant Plains" etc.) so the player knows who they're fighting before they swing. + +### Bug Fixes +- **Captured bosses now actually show up in `,buddy storage`**: The capture INSERT referenced the new `boss_zone_id` column which only exists after migration 0263 runs. On a database where the migration hadn't applied yet, the INSERT silently failed and the buddy never made it into storage. Capture now tries the full INSERT first and falls back to the column-less INSERT + a separate UPDATE if the column is missing -- so the buddy is always saved, and the boss-variant cosmetic + ability override re-attach automatically once 0263 lands. `list_storage` also gained a graceful fallback on the SELECT so storage works mid-deploy. +- **Procedural portraits: removed translucent muzzle trapezoids on Fox + Wolf**: The cream-coloured snout polygons sat directly over the mouth line and read as ghostly trapezoids pasted onto the face. Replaced with a small nose dot at the bottom-centre (plus two white fangs for the wolf), and the mouth + eyes moved into a clean vertical stack on the head's centre axis. +- **Thornling has actual legs now**: The previous "vines underneath" was just two faint arc strokes that looked like the buddy was floating on a circle. Replaced with a pair of stubby root-trunks anchored to the base of the body, each splitting into three toe-roots planted on the floor. +- **Buddy arena: travel is now actually two-way**: Every `ARENA_ZONES` neighbour edge is auto-mirrored at module load. Players who reached Grassy Meadow / Windmill Lane / Plains Arena can finally walk back to Plains Gate, and the whole map can be retraced instead of being a one-way river. A new `arena_graph_asymmetries()` diagnostic raises on any future one-way edge that slips into the config. +- **Buddy arena: bosses cannot be skipped via a wild fight**: `,buddy map battle` at a boss zone is rejected; only `,buddy map boss` clears the zone. `on_zone_cleared` now takes `is_boss_clear` and refuses to advance region unlocks when a boss zone was "cleared" by a wild encounter (the bug players reported as "boss wasn't even there and I won and cleared the zone"). +- **Buddy arena: killing blow ends the battle in one click**: Added a defensive early-resolve check on every battle button so a round that drops HP to 0 immediately triggers `_resolve_battle` instead of leaving the view alive for a phantom extra hit. +- **Buddy arena: zone-appropriate spawns**: New `ZONE_WILD_POOLS` table assigns themed species to every combat zone -- crabs and shrimp stay in the tide, cobble and wolves on the mountain, thornlings in the forest, blazers in the volcano. Boss zones use `ZONE_BOSS_SPECIES` so each region's boss is a recognisable foe instead of a random "Wild X". No more shrimp on the mountain. +- **Buddy portraits: fixed anatomy on Cobble, Fox, Wolf, and Zenny**: Cobble was a stack of disconnected circles topped by a tiny head; now a single rounded boulder with the face carved into the upper third and stub limbs. Fox and Wolf had snouts protruding sideways with the nose drifting off the side of the head; both redrawn front-facing with centred muzzles and symmetric ears. Zenny had a hooked parrot beak and a single wing floating off one side; now a duckling-style centred beak with symmetric folded wings. + +### Changes +- **Buddy arena: zones support multi-clear**: Normal zones now require three clears to be marked permanently cleared (`clear_target`, default 3); boss zones still resolve on a single win. Repeat clears continue to award scaling drops and BUD/BBT so revisits stay valuable. Tracked via `cc_buddy_zone_trophies.required_clears` (new column, migration 0262). +- **Buddy arena: per-move animation overlays**: Strike / Special / Risky / Brace each render their own attack-burst overlay during the in-battle FPS burst. Strike keeps the yellow slash arcs; Special wraps the attacker in a blue energy ring and fires a radial burst at the target; Risky paints diagonal orange motion blur with an 8-point red impact star; Brace pulses a green shield ripple around the bracer instead of striking at all. Frame count and timing unchanged, so the per-move flavour fits inside the existing 0.7s burst. +- **Buddy arena: specials region added to ARENA_REGIONS**: New `forest` and `volcano` region rows (boss zones Druid Circle and Magma Caldera) plus the `special` region for non-combat nodes. The tournament still unlocks on the original 3 region bosses (Plains, Stone, Tide); Forest + Volcano are post-launch expansion content and don't gate the bracket. +- **Buddy arena map render: wider canvas, non-overlapping bands, special icons**: Map canvas grew to 1600x1080. Region bands are now laid out as a clean two-row grid (Forest + Volcano + Tournament across the top, Plains + Stone + Tide across the bottom) so the Caldera Ridge panel no longer eats half of Tideway Coast, and Tide Amphitheatre / Champion Hall sit inside the right rectangles. Specials render as rounded squares with kind-specific glyphs ($ shop, ~ spring, * dig, ? trader) instead of generic combat dots; legend updated to match. +- **Map legend no longer overlaps Whispering Forest**: Repositioned the legend into a two-row horizontal banner just below the title; Whispering Forest band starts at y=200 so the region header label is fully readable. Champion Tournament panel widened from 180px to 270px so the title isn't cut off; renamed to "Champion Hall" inside the panel since the full "Champion Tournament" was wrapping past the right edge. Tournament status pill moved to clear the top-right corner. +- **Delve battles now use the buddy-battle scene PNG**: Both wild-buddy and regular mob fights inside `,delve` render the same Pokemon-Stadium-style scene PNG that `,buddy map battle` ships, with level pills, HP bars, action banner, and round counter. Battle log + summary embeds also match the buddy-battle layout (image + Rounds played footer) so the post-fight screen reads the same whether you killed a Wolf in the arena or a Skeleton in a dungeon. +- **Delve battles surface level + rarity + kind during combat**: The mid-fight panel now shows "Lv X Common/Uncommon/Rare/Epic/Legendary" plus a kind label ("Wild Buddy", "Beast", "Undead", etc. when the mob meta has one) for the opponent, and "Lv X " for the player. No more "Mystery Mob \U0001F4A4" without context. +- **Per-move animation overlays in PvP + wild buddy battles**: `,buddy battle` (PvP) and wild buddy fights in `,delve` now play the same four-frame attack burst that the arena map uses -- Strike (yellow slash arcs), Special (blue energy ring + radial burst), Risky (orange motion blur + 8-point red impact star), Brace (green shield ripple on the bracer). Each fighter's swing fires before its hit resolves so the visual matches the log line. View defers the interaction up front so the burst frames don't blow past Discord's 3s response window. +- **Battle scene PNG now used across EVERY buddy-battle entry point**: New shared `services.buddy_battle_scene.fighters_to_scene_state(player, enemy, ...)` helper builds the scene-state dict from any pair of Fighter-shaped objects. Every interactive battle view now renders the same Pokemon-Stadium PNG: `,buddy battle` (PvP), wild buddy fights from `,buddy world`, fishing wild buddy fights from `,fish cast`, delve mob battles AND delve wild-buddy battles, arena map + boss + tournament, AND farming pest battles. Every `_round_embed` returns `(embed, scene_file)` and the corresponding views attach the PNG via `attachments=[file]`. PvP+wild bursts (strike/special/risky/brace overlays) also ride the same renderer so animations look identical regardless of which game surface spawned the fight. +- **Per-move animation overlays now play in fishing, delve wild, and farming pest battles too**: New shared `services.buddy_battle_scene.play_battle_action_burst(view, player, enemy, ...)` helper paints the same four-frame attack-burst overlay (yellow slash / blue energy ring / orange motion blur / green shield ripple) on any battle view. `cogs/buddy.py:_play_pvp_action_burst` was refactored to delegate to it; fishing `_WildBattleView._handle_action`, delve `_DelveWildBuddyView._handle_action`, and farming `PestBattleView._resolve` now all play a player-swing burst followed by an enemy-swing burst per round. Farming only fires the burst on `attack` (capture/flee don't have a swing visual); player HP shows full since farming combat doesn't track it. Every view defers the interaction up front so the burst frames don't blow Discord's 3-second response window, then edits the message directly for the final round panel. +- **`,buddy arena` now uses the unified battle scene + per-move bursts**: The legacy Buddy Network arena view (`_BuddyArenaView`) was still painting the old text-only card embed. Updated `_round_embed` to return `(embed, scene_file)` with the same Pokemon-Stadium-style PNG every other buddy battle now ships, and `_handle_action` now defers the interaction, plays a player-swing burst + enemy AI swing burst (strike-flavoured) per round, then edits the message directly for the final round panel. +- **Boss-tamed buddies get unique ASCII art on `,buddy` panel**: `cogs/buddy.py:_current_frame` now checks `BOSS_ASCII_FRAMES[boss_zone_id]` before falling back to the species frame table. A captured Meadow King shows a crowned-wolf silhouette (with full happy / neutral / attack / hurt / victory / down / using_item poses), Granite Champion a horned-helm boulder, Tideborn Sovereign a crowned crab, Bramble Druid an antlered thornling, Caldera Tyrant a flame-maned blazer. The PNG portrait + ability override + ASCII art all key off the same `cc_buddies.boss_zone_id` column so they stay consistent. + +### New Features (cont.) +- **AI chat queue with per-user serialization and live queue feedback**: The chat AI now runs through a proper per-backend queue (`core/framework/ai/queue.py`) instead of a single 32-wide global semaphore. OpenRouter and Ollama have their own slot caps (`AI_QUEUE_OPENROUTER_CAP=24`, `AI_QUEUE_OLLAMA_CAP=2` by default), a reserved sub-pool keeps background commentary from starving user-facing chat, and a per-user lane serializes one user's own requests so power-users can't queue-bomb other users into "AI didn't respond" timeouts. The `,ask` placeholder now shows live position ("queued (#3)") that decrements as the user advances. New `,ai queue` admin command snapshots current depth per backend. +- **Regenerate + Try-harder buttons on AI replies**: Every `,ask` and reply-to-bot AI message now ships with a Regenerate button (re-run the same prompt) and a Try-harder button (re-run with temperature bumped by `AI_REGEN_TRY_HARDER_TEMP_BUMP`, default +0.35, capped at 1.5). Only the original asker can click. State is held in-memory for `AI_REGEN_TTL_S` (default 15 min) before the buttons auto-disable. The Sources button (when web search returned citations) is folded into the same view so users keep both affordances. +- **Richer chat AI placeholder with phase, tool, and footer info**: The 4-frame half-circle spinner is replaced by a 10-frame braille spinner that animates more smoothly under Discord's edit-throttle. The placeholder now shows phase text ("queued (#3)" / "thinking" / "running data.web_search" / "wrapping up"), a sub-line listing completed tools with check/cross markers, and a small-text footer with model + tool count + elapsed + token usage on the final reply. Logic moved into a new `cogs/_chat_status.py:ChatStatusRenderer` so every AI entry point (`,ask`, reply, mention) gets the same UX automatically. +- **Autonomous passive trait learning from every chat turn**: When `AI_AUTO_LEARN_ENABLED` is on (default), every meaningful chat turn fires a cheap LLM extraction call against the (user msg, assistant reply) pair and upserts candidate traits into `ai_user_traits` with `source='passive_chat'` and a low 0.3 confidence seed. The existing decay + behavior-shift promotion logic handles cleanup of one-shot misreads and naturally promotes repeated signals to the `stable` layer, so memory tracks the conversation in close to real time instead of waiting on the 4h / 40-msg refresh cadence. Per-user rate limits (`AI_AUTO_LEARN_EVERY_N_TURNS=3`, `AI_AUTO_LEARN_MIN_INTERVAL_S=600`) keep token cost bounded. Opted-out users (`,optout`) and turns where Ollama already has users waiting are skipped, so the learner never steals queue capacity from chat. +- **Wealth Bottleneck replaces the wealth tax and yield throttle**: No more overnight drains on stones, bags, rigs, NFTs, savings deposits, validator stakes, delegations, LP positions, mining rigs, moon stakes, or gamba stakes -- every existing holding is now permanently off-limits. Instead, every fresh USD-equivalent gain you earn (work, beg, ape, daily, faucet, drops, realized USD profit on trade sells, gamba yield, stake / LP / PoS / delegation / mining / network-claim / savings interest) is scaled by your leaderboard rank: poorest **x1.50** boost, median **x1.00** neutral, richest **x0.10** drag. Drag from the top funds a per-guild community pool; boost to the bottom is paid out of the same pool. When the pool runs dry, boosts pause until it refills -- no value is ever printed. Every credit's embed shows the multiplier and the USD that was diverted to or sourced from the pool so nothing happens silently. See `,bottleneck`, `,help bottleneck`, and `,economy` -> Health tab for the curve, pool, and per-user history. +- **`,bottleneck` command (alias: `,wealth`, `,bn`)**: Player surface for the bottleneck system. `,bottleneck` shows your current rank/multiplier/pool/last-7d-flow with your dot on the curve; `,bottleneck curve` prints the full multiplier curve; `,bottleneck pool` snapshots the per-guild USD pool with a 24h flow recap; `,bottleneck me` lists your last 14 days of drag/boost plus your 10 most recent credits; `,bottleneck recent` shows the last 25 events across the guild. +- **Trade sells now use realized USD gain, not session principal**: Single-token `,trade sell` and `,trade sell everything` route the realized USD profit (sell fill price minus weighted-average buy price from the user's last 100 BUYs) through the bottleneck. Losing or break-even sells are not throttled. Cost basis is derived from the existing `transactions` ledger so no schema change was needed. + +### New Features +- **AI chat: per-user running-gag flavors (Wecco loosened + 8 new regulars + Elma Agnes catchphrase)**: Restructured `services/ai_easter_eggs.py` from a one-off Wecco full-voice override into a unified `_FLAVORS` table that adds a SHORT (2-4 sentence) flavor block to the system prompt when one of the known regulars is talking to Disco OR gets mentioned by someone else. Wecco's old "peak-cringe UwU dialect every message" override is now a light "dust a couple of references on top, keep your normal voice" flavor (matches the rest in tone density). New regulars: Zoro (constantly making alts + denying it), Bluethunder (James-Cameron-Avatar superfan), Swaglord (schizo-coded, needs his meds), Six (host of 'We Live in the Dumbest Timeline'), The Comedian (German, sarcastically 'loves' eastern German politics), Mikk (insists he's chat level 100 -- isn't), Jess (hates AI -- Disco gets slightly self-deprecating with her), Callum (food-posts constantly, :poglum: emoji), and Elma Agnes (no real user id -- a fake-scammer running joke triggered by name OR her catchphrase "Greetings, Are conditions in the cryptocurrency market presently conducive for investment?"). Adding a new flavor: bump `_FLAVORS` with one `_Flavor(name, aliases, speaker_flavor, mention_flavor)` row -- the chat pipeline picks it up automatically. +- **AI chat: "Continue" button on truncated replies**: When the model hits its `max_tokens` cap mid-thought (or the rendered body exceeds Discord's 2000-char message limit), the reply view now ships with a third button -- `▶ Continue` -- next to Regenerate / Try harder. Clicking it sends a follow-up message in the same channel that picks up exactly where the model left off (the original message stays as the head of the thread). The continuation gets its own reply view so users can chain multiple Continues if the model truncates again. Detected via `finish_reason="length"` from the OpenAI-compat API surface in `core/framework/ai/client.py`, threaded through the bridge's `done` event and into `_AskState.was_truncated`. The button is hidden entirely when the reply ended cleanly. + +### Bug Fixes +- **AI chat: root-cause fix for cut-off replies + missing footer + missing buttons**: The chat renderer was burning the channel edit-bucket (5 edits per 5s per channel) by editing on every streaming delta event — measured at 7.7 edits per 5s for a single reply, well over Discord's limit. Multiple concurrent replies in the same channel compounded this, pushing the final `finalize` edit (the one that lands body + footer + view) behind a queue of throttled delta paints; if the outer `wait_for` timed out, the placeholder was left stuck at the last partial-buffer paint, producing exactly the symptom users were screenshotting (mid-sentence cutoff, no `-# model | tokens | elapsed` footer, no Regenerate / Try-harder buttons). Fix in `cogs/_chat_status.py`: streaming delta events now accumulate silently into the renderer buffer with NO per-delta edit, the spinner animator throttle is 1.6s (was 0.85s), and `finalize` is a single-attempt full edit with explicit content-only fallback when the view payload is rejected or rate-limited. Cog adds a `_patient_view_attach` helper with `(0, 1s, 3s, 6s)` backoff to recover the regen + sources view when finalize had to drop it. Stress tested at 1, 2, 4 concurrent replies in the same shared edit bucket: 100% of finalize states (body + footer + view) land correctly; even an 8-concurrent torture test lands 7/8. Per-reply edit budget is now ~2 edits over a typical 5s response, leaving headroom for several concurrent replies. +- **AI chat: same-user mentions no longer pile up behind each other**: The original chat queue serialized one user's own requests (`@mention` x4 in a row queued at "queued (#1)" while the first one ran, then the second only started after the first finished). On a single-user / small-server deployment that produced visible "queued" placeholders that never advanced, plus partial cut-off text on the in-flight reply when the wait_for finally fired. Removed the per-user gate from `core/framework/ai/queue.py:_is_eligible`; only the per-backend cap (`AI_QUEUE_OLLAMA_CAP=24` etc) gates concurrency now. Verified with a 4-same-user smoke test: all four start at the same tick instead of sequentially. +- **AI chat: regenerate buttons now actually show up on every reply**: The `,ask`, reply-to-bot, and `@mention` paths all wire a regenerate / try-harder / sources view onto the placeholder, but two separate `placeholder.edit()` calls were racing: the streaming finalize landed first with `view=None` (or `view=sources_view`), then a follow-up edit re-attached the regen view. Under burst conditions Discord intermittently dropped the second edit's view, so users saw the reply without buttons. Collapsed both into a single `renderer.finalize(body=..., view=final_view)` call via a `final_view_factory` callback so the body, footer, AND regen view land in one edit. Also wired the @mention path which was missing the regen view entirely (only `,ask` and replies had it). +- **AI chat: cut-off responses retried instead of left mid-sentence**: When the channel's edit-rate bucket was saturated (multiple parallel chat replies in the same channel), `ChatStatusRenderer.finalize`'s single `placeholder.edit` could silently fail on a transient HTTPException, leaving the reply stuck at the last partial-buffer paint from the streaming animator (e.g. `"i'm literally right here. you're just"` with no buttons). Now retries up to 3 times with `(0, 0.5s, 1.5s)` backoff and only gives up on hard 4xx errors that won't heal. + +### Changes +- **AI chat queue: Ollama gets the bigger lane by default**: This deployment uses Ollama as the primary backend, so the per-backend caps are flipped: `AI_QUEUE_OLLAMA_CAP=24` (up from 2), `AI_QUEUE_OPENROUTER_CAP=8` (down from 24). Operators who run the opposite mix can swap them back via env vars. +- **Removed legacy global semaphore in `core/framework/ai/client.py`**: Every backend call now routes through `chat_queue.acquire(...)` instead of `async with _request_semaphore`. The previous `_MAX_CONCURRENT_REQUESTS = 32` constant is gone -- per-backend caps in `Config.AI_QUEUE_*` are the real flow-control point now. Vision endpoints (`complete_ollama_vision`) use the Ollama `system` lane so a vision tool call never blocks behind queued chat traffic from another user. Added migration `0261_ai_user_traits_source.sql` for the new `source` column on `ai_user_traits` and threaded `source` + `confidence_seed` kwargs through `db.upsert_ai_trait` and `services.ai_traits._ingest_signal` so passive-chat-extracted traits can be queried apart from tone / reaction / behavior signals. +- **Removed legacy wealth tax, UBI cycle, CWE controller, streaming UBI, and game-token burn phase**: The daily `wealth_equalizer` cycle (which sold off stones, CeFi bags, DeFi bags, validator stakes, delegations, moon stakes, gamba stakes, and mining rigs from rich players), the CWE per-tx tax + Gini PI controller + bonus-cap state, and the game-token burn phase (which decremented `circulating_supply` from holders above per-token thresholds) are all gone. Migration `0260_bottleneck.sql` drops the underlying tables (`wealth_redistribution_pool`, `wealth_redistribution_log`, `economy_health_snapshots`, `cwe_curve`, `cwe_controller_log`, `cwe_user_tx_state`) and creates `wealth_pool` + `bottleneck_log`. Removed services: `services/wealth_equalizer.py`, `services/cwe.py`, `services/cwe_render.py`, `services/token_health.py`, `services/lp_restore.py`, plus `cogs/wealth_equalizer.py`. Renamed `services/equalizer_charts.py` to `services/drs_charts.py` (kept the still-used `render_value_bars`, `render_winloss_bars`, `render_timeline` helpers; dropped the gini/pool/user-history renderers). Removed config keys: `WEALTH_EQUALIZER_*`, `WEALTH_TAX_*`, `WEALTH_UBI_*`, `WEALTH_YIELD_*`, `CWE_*`, `GAME_TOKEN_BURN_*`, `DAILY_SCALING_ENABLED`, `GENESIS_LP_RESERVE`. Added: `BOTTLENECK_ENABLED`, `BOTTLENECK_CURVE`, `BOTTLENECK_MIN_HOLDERS`, `BOTTLENECK_MAX_BOOST_MULTIPLE_OF_GROSS`. +- **`,help wealth`, `,balance` profile tab, `,profile` footer, and `,economy` Health tab now describe the bottleneck**: All player-facing copy has been rewritten away from "wealth tax / UBI / yield throttle" to "Wealth Bottleneck / community pool / leaderboard rank". The `,balance` profile tab adds a new Bottleneck row showing your current multiplier; `,profile` appends your multiplier to the net-worth footer; the `,economy` Health tab now shows the bottleneck curve + community pool instead of Gini / top-N concentration / token-burn audit. Removed `,drs equalizer` (and its `cycles`, `cycle`, `user`, `chart`, `export` subcommands) since the audit tables it read are gone; per-user audit data now lives in `,bottleneck me`. +- **Removed `,admin lp_restore`**: The pass refunded LP-tax victims out of `wealth_redistribution_pool`; both the audit log and the pool are gone, and the bottleneck never touches LP positions in the first place, so the command had nothing left to do. + +### Bug Fixes +- **Fix fox snout/nose drawn on the side of its head**: The nose ellipse was being painted at `head_cx + 0.18..0.24` while the snout polygon only reached `+0.22`, so the nose center sat behind the snout tip and visually clipped onto the cheek. Extended the snout tip to `+0.26`, recentered the nose ellipse exactly on the snout tip with a smaller bounding box, and pulled the size out into named `nose_rx` / `nose_ry` so the same draw scales cleanly at 320 (battle) and 480 (standalone) sizes. +- **Fix `,buddy map battle` crash on `active` column lookup**: The arena view's active-buddy fetch referenced a non-existent `active` column, so every map battle / boss / travel attempt failed with `column "active" does not exist`. Switched the query to the canonical `is_active` column to match the rest of the buddy stack. + +### New Features +- **`,buddy map battle` and `,buddy tourney fight` now use the full Strike / Special / Brace / Risky action set**: Map and tournament battles used to expose a single **Attack** button alongside the consumable dropdown, which threw away every species spell and made every fight feel identical. Replaced with the same four-action bar the PvP duel uses -- **Strike** (+1 stamina), **** (the buddy's actual species ability, costs 2 stamina), **Brace** (heal 8% + halve next hit), and **Risky** (60% big hit / 25% miss / 15% backfire). The Special button is labelled with the buddy's literal ability name (e.g. "Pack Howl" for a wolf, "Ink Cloud" for a shrimp, "Preen" for a wecco) and is greyed out when stamina is short. Stamina pips and the brace flag are now surfaced in the round footer so the player can see what they've got. All four actions route through the engine's `_apply_hit` + `_maybe_proc_on_hit` helpers so lifesteal, static_shock, reflect, counter, low_hp_rage, preen, berserker, second_wind, killing_blow, and every other ability fires the same way in zone / boss / tournament / wild fights as it does in PvP. +- **Snappier battle animation**: Per-frame interval cut from `0.45s` to `0.18s` and burst frame count from 6 to 4. A single attack burst now plays in ~0.7s instead of 2.7s, which is well under Discord's 5-edits-in-2s ceiling and stops the fight from feeling like it's loading a CD-ROM. + +### Changes +- **Single source of truth for combat action tuning**: PvE (map / boss / tournament) and PvP both read Strike / Special / Risky / Brace damage windows, stamina cost, and stamina cap from `services/buddy_battle.py:PVE_*` constants. The old `_PVP_STRIKE_RANGE` / `_PVP_SPECIAL_RANGE` / `_PVP_RISKY_RANGE` / `_PVP_BRACE_HEAL` literals in `cogs/buddy.py` are gone -- changing a tuning value in one file now applies to every battle type, per the CLAUDE.md "no duplicate logic" rule. +- **`,buddy map battle` parity with `,buddy arena`: rich intro + BUD/BBT rewards**: The map battle intro now mirrors the arena's pre-fight surface so the player can see what they're committing to before swinging. Embed shows both fighters' stat blocks (level, HP/ATK/SPD with mood multipliers, abilities, W/L), the zone's L-band, current region progress (zones cleared and region bosses toward the tournament gate), a BUD + BBT win-reward preview with first-clear vs repeat-clear math, the item drop preview with hit chance, the combat move cheat sheet, and the bag size. The win embed shows the actual minted BUD + BBT amounts and the item drop emoji. +- **Map battle rewards now mint BUD + BBT, not DSD**: Every zone clear credits the Buddy Network wallet directly via the same paths the arena uses (`mint_bud_reward` applies the standard mint-impact oracle drop; `mint_bbt_reward` mints clean since BBT is price-discovered through cashout / burn-for-bud, not from arena/zone mints). Rewards scale with zone progression -- Plains Gate first clear pays ~2.5 BUD + ~15 BBT; Tide Amphitheatre boss first clear pays ~145 BUD + ~820 BBT; Champion Hall pays 250 BUD + 1,500 BBT. Repeat clears pay 25% of the first-clear amount. Boss zones get a flat +25 BUD / +100 BBT cherry on top. The "+$X DSD" line that the win embed used to print was a stale display -- zones never actually credited DSD, but the new flow makes the BUD + BBT credit real. +- **Drop Pillow portrait from `,buddy` panel, keep ASCII frame**: The main `,buddy` embed no longer uploads `buddy_portrait.png` -- the in-embed ASCII frame is the only visual on the panel now. Removes one upload per panel send and keeps the frame consistent with the rest of the buddy displays. +- **Per-species buddy portraits + fix battle-scene mirror text**: The arena scene was rendering the right-hand fighter's portrait with `FLIP_LEFT_RIGHT`, which inverted the in-portrait `Lv X` / mood pill so "DOWN" came out as "NWOD" and "Lv 3" looked reversed. Dropped the in-portrait text pills entirely (the battle scene already paints name + level OUTSIDE the portrait at the top of each side). Replaced the shared "ellipse + decoration" silhouette with per-species draw functions in `services/buddy_portrait.py` so a fox actually reads as a fox (snout, tall ears, bushy white-tipped tail), a shrimp reads as a shrimp (curved segmented body, antennae, fan tail), a crab has eye stalks + claws + walking legs, an octopus has a bulb head + eight tentacles, etc. Covers fox, wolf, shrimp, lobster, crab, octopus, zenny, pyper, cobble, wecco, nimbus, blazer, thornling, draclet, glitch. + +### Discord Bot +- **Wire Buddy Battles expansion into ,buddy group + attach Pillow portrait**: Two real bugs were keeping the expansion invisible in-game:; 1) The new cog was never added to core/framework/bot.py's INITIAL_EXTENSIONS,; so the entire arena / tournament / consumable surface never loaded (`73867e50`) +- **Wire 6 more mastery effect consumers across dungeon/exploit/trade/bank/buddy**: Adds consumer sites for nodes the earlier expansion left as data-only.; Every consumer is a single try/except wrapped passive read so a; failure never blocks the underlying action. (`12c74aa5`) +- **Buddy Battles expansion: arena map, tournament, consumables, Pillow scenes**: Adds a Pokemon-Stadium-style metagame on top of the existing flat-queue; buddy arena: a 14-zone branching map across 3 themed regions; (Plains/Stone/Tide) with two hidden side zones, a 4-round Champion (`62cb55ee`) +- **Fix payout overlaps, drop phantom Lv 1, wire per-action mastery XP**: Four player reports rolled into one pass:; 1. ",work / ,ape / ,beg cards look like shit" -- badge sat on the; avatar's frame ornament and the net-reward pill sliced through (`89e73ca9`) +- **Retune mastery XP curve + fix pillow card overlaps**: Player reports:; - "I did one delve battle and am immediately raider lvl 20"; - "Raider L27 ... I've hardly delved at all ... 28 points available (`031a12bc`) +- **Harden hidden mention override against post-resolution content**: addendum_for_mentions now also accepts an explicit mentioned_user_ids; list -- the cog populates it from message.mentions, which is Discord's; canonical parsed mention list. The previous implementation relied on (`943d3b3a`) +- **Fix ,work C_AMBER crash + real cosmetics art + titles that mean something**: - ,work: dropped the redundant `from core.framework.ui import C_AMBER, C_PURPLE` inside the cosmetic-render try in cogs/earn.py:1343. It shadowed the module-level import so any exception before that line left C_AMBER unbound and the except path raised UnboundLocalError.; - Sigils: new core/framework/sigil_art.py with per-id procedural draw functions for every themed sigil (cat, moon, turtle, five_star, tidewave, cross_bones, dice, gavel, cards, eagle) plus mastery-track and legendary ones (phoenix, dragon, infinity). Mirrors core/framework/banner_patterns.py shape; wired into profile / level / payout renderers. Cat sigil is now an actual cat silhouette, not "C in a circle".; - Frames: new core/framework/frame_art.py layers per-id flair around the avatar disc -- tabby claw notches, crescent moon, shell bumps, comet trail, anchor chain, suit pips, eagle laurel, diamond facets, obsidian triple-ring, halo glows for ember / frost. (`d379ae06`) +- **Fix ,tour Next jumping to end on rerun and ,apex float tzinfo crash**: - Tour: navigation cursor now lives on _DeckView; renamed onboarding_deck.advance to set_progress(idx) so a Next press always advances exactly one card past whatever's on screen instead of re-reading stale "completed_at" state out of the DB.; - Apex: route every "started_at"/"ends_at" through fmt_rel/fmt_ts (which already accept epoch floats and datetimes); event_poster._time_progress and _format_remaining coerce through a single _ends_at_epoch helper so floats no longer hit ".tzinfo".; - Mastery board: replaced the hard 32-char slice with a pixel-accurate greedy word-wrapper, ellipsis-truncate names that would overflow into the cost pill, widen cards and bump title/description font sizes so descriptions stop bleeding through the box. (`5d15650c`) +- **Replace ',profile shop' page-number nav with buttons + theme dropdown**: Player feedback: typing ',profile shop 1' then ',profile shop 2' then; ',profile shop cats 2' to walk pages and switch themes is "absolutely; horrible". Replaced with a discord.ui.View attached to the embed: (`142bc71b`) +- **Rebrand ,tour as Discoin Tour and expand mastery + apex info commands**: Player feedback: ',tour' was titled "Apex Onboarding" with 5 thin cards,; ',mastery' said almost nothing about what mastery actually is, and; ',apex' had no way to learn about events without one being live. (`b4356d2d`) +- **Profile shop pagination + rarity + pixel-art banners + flesh out profile card** (`4ac5db83`) +- **Rename V3 onboarding to ,tour -- ,start is claimed by overview cog**: Bot crash-loop on startup:; CommandRegistrationError: The command start is already an existing; command or alias (`d50c0967`) +- **Profile fix: drop showcase alias collision + obsidian default + show job+level**: User feedback: ',profile' was showing 'Novice -- DeFi Degen -- Level 1'; and they didn't recognise where any of it came from. Two compounding; issues + a defaults overhaul: (`c1d37eea`) +- **10/90 coin/token split + cosmetic-themed PNGs on daily/work/ape/beg**: Two complaints fixed:; 1. Game payouts were minting wildly off-ratio coin/token pairs. A; single fishing event could land $6,309 LURE for $0.07 REEL once (`9adc67a5`) +- **Hotfix CWE production failures + overflow guard on pool rebalance**: Symptoms from first-day prod log:; - Every UBI stream tick: "cwe ubi stream: payout failed gid=... uid=..."; with no diagnostic, including uid=0 (the protocol reserve sentinel). (`9b8b9eb0`) +- **V3 wiring batch + cosmetic shop + Pillow ,level card**: Wiring (one-line hooks at high-volume call sites):; - cogs/earn.py daily + work: gross reward routes through; services.cwe.tax_on_credit() so the percentile-based per-tx tax + (`040ebcb8`) +- **V3 Pillar 7: ,start onboarding deck (5 Pillow PNG cards + progress persistence + Next/Skip view + 0242 migration)** (`a1956f40`) +- **V3 Pillar 3: Clan Wars (Apex Conflict 12-node board, 60s heartbeat, Pillow 1600x900 map, 0238 migration)** (`ac8c8b20`) +- **V3 Pillar 4: profile cosmetics (Title/Banner/Frame/Sigil + ,profile PNG card + gallery + 0239 migration)** (`2ef9ec1a`) +- **V3 Pillar 6: Apex Events (cross-system world events with 7-event catalogue, Pillow posters, 30s heartbeat roller, inbox broadcast)** (`e1f32bac`) +- **V3 Pillar 5: persistent in-bot inbox (services/inbox.py + cogs/inbox.py + 0240 migration + Pillow renderers)** (`93e49bae`) +- **V3 Pillar 2: Apex Mastery (cross-system meta-progression)**: Binds nine minigame XP islands (fisher / farmer / delver / trader /; gambler / raider / tamer / validator / crafter) into one career: each; emits mastery XP, leveling grants mastery points, points unlock (`4c9215a8`) +- **V3 Pillar 10: Continuous Wealth Equalizer (CWE)**: Replaces the daily tax cycle with per-tx tax + streaming UBI while; keeping the audit trail identical so every existing ,wealth and; ,drs equalizer surface keeps working. (`4cf3ad2f`) +- **V3 Pillar 9: LP tax exemption + one-shot historical restoration**: LP positions are permanently exempt from the wealth tax model. The; position itself stays in compute_bulk_net_worth so every leaderboard; and profile still shows it; the carve-out only applies to the (`8958aca6`) +- **Fix ,balance Items tab + Net Worth: stones priced at oracle, all stones shown**: Three compounding bugs around stone display + valuation:; 1. services/net_worth.py was summing every stone's staked_amount as; to_human(...) priced at $1, but migration 0165 narrowed: (`f13f4c9e`) +- **Soften Disco voice: less degen, slightly less tired**: Reworded the core _DEFAULT_ASK_PROMPT identity and the degen / AI-channel; persona hints so Disco still has personality but doesn't read as a burned-out; 4am crypto-native. "crypto-native" -> "crypto-fluent", "a bit dry / sometimes (`7826e02b`) +- **Fix CI alias collision + DRS round 5 (prefs / locks / token-wide audit)**: CI fix:; ,drs eq cycles and ,drs eq user both declared "history" as an alias.; Both live in the same drs_eq subgroup namespace so discord.py raised (`35b40e93`) +- **DRS round 4: daily / items / wallets / exploit / guild**: Five more ,drs subcommands cover the account-flavour audit angles.; - ,drs daily (alias streak / daily-streak); Streak + last_daily + 24h eligibility computed against the DB (`f2104740`) +- **DRS surfaces round 3: lunar/moon, safety, disc.fun, NFTs, delve, buddy, crafting, savings, trades, work, full net worth**: Twelve more ,drs subcommands close out wealth-surface coverage so; every category that contributes to compute_net_worth has a dedicated; audit view. (`3d8fcae1`) +- **DRS full-surface x-ray: stakes/validator/mining/lp/stones/gamba/games/loan/timeline**: Nine new ,drs subcommands give auditors visibility into every; wealth-bearing surface on the bot, each rendered with a Pillow; chart attachment where it makes sense. (`a35732b0`) +- **DRS Equalizer x-ray: paginated cycle history, charts, CSV export**: New DRS Terminal subgroup that exposes every redistribution event since; genesis to trusted auditors. Tied to the existing wealth_redistribution_log; and economy_health_snapshots tables -- no schema changes. (`3de4757e`) +- **Wealth Equalizer: rank-progressive tax + pay-everyone UBI + close hideaways**: Fixes "I got taxed but didn't get any money" and pushes the redistribution; hard enough to actually pull Gini down from 0.991.; Tax: holders are sorted richest first; a rank multiplier (2.0x at #1, (`75d36c52`) + +### Tests +- **Fix CI: add battle kind to _VALID_APPLY_KINDS test allowlist**: The Buddy Battles expansion added a new 'battle/' crafting apply; route that deposits into user_buddy_economy.battle_inventory. Eight; new recipes use the prefix (berry_quick_craft, vial_rage_craft, ...) (`4edcb4c5`) +- **Steepen mastery level curve so mid-tier takes commitment**: Followup on the per-action XP rebalance. Bumped TRACK_BASE_XP; 100 -> 300 (3x) and TRACK_XP_GROWTH 1.15 -> 1.22 (steeper) so the; same XP grants buy fewer levels: (`09d3311c`) +- **Use @pytest.mark.asyncio in test_title_passives instead of homegrown runner**: The earlier homegrown _run() helper called asyncio.get_event_loop(); inside a fallthrough branch; in Python 3.12 (which the CI uses) this; raises "There is no current event loop in thread 'MainThread'" (`0bfe2eb4`) +- **Fix CI: update trade-oracle test to assert V3 Pillar 8 behavior**: The pre-V3 test ``test_sell_does_not_update_oracle`` pinned the; broken behavior Pillar 8 fixed: that ``execute_sell`` would NOT; move the oracle even on large dumps. V3 inverts that -- both (`b47344c3`) + +### Services +- **Fix apex_events.expire_finished crashing on every expire tick**: The DELETE ... RETURNING + INSERT pattern read started_at / ends_at; back into Python (where core.database._coerce turns TIMESTAMPTZ; into an epoch float for the convention every renderer relies on) and (`1a7482fb`) +- **Add hidden per-user persona overrides for chat AI**: services/ai_easter_eggs.py is the one place where Disco's voice gets; bent for a specific user instead of a channel / role tag. Two surfaces:; a "speaker block" injected when a tagged user is the one talking, and a (`9d27e340`) +- **V3 Pillar 8: swap-impact oracle fix (chart moves on buy/sell)**: services/trade.py buy/sell flows now call apply_trade_oracle_impact(); after the atomic block, so market orders actually move crypto_prices; and write a candle row. Pre-V3 the chart stayed flat even after huge (`b71fb77f`) + +### Bug Fixes +- **,admin reset user now wipes every V3 economy table**: reset_user was deleting from a hard-coded table list that hadn't; been updated since pre-V3, so resetting a player left their; Safety Module, Disc.Fun, NFTs, Farming, Buddy, Crafting, Delve, (`dbfc1708`) +- **Gamba_stakes pending column rename - tax actually was NOT seeing gamba**: The previous "gamba tax fix" commit wrote SQL against pending_gbc but; migration 0234 had already renamed that column to pending_yield_raw; and added yield_target (default 'GBC', opt-in 'BUD'). Every call site (`4bd0b499`) +- **Cut Ollama timeout failures and surface the actual reason**: Screenshot from prod showed gemma4:31b-cloud working in 2 of 5 replies; (8.4s and 27.3s, 8-10k tokens each) and bouncing in the other 3 with; the generic "AI didn't respond. Try again in a sec." card. Three (`22ba197b`) +- **Route disco chat to Ollama when TOOLS_BACKEND=ollama (no more silent 400s)**: User reported "AI didn't respond" cards in chat with an Ollama-configured; guild. Log showed:; [ai/stream] OpenRouter HTTP 400: {"error":{"message":"gemma4:31b-cloud (`25b687bd`) +- **Wealth tax now sees and drains gamba stakes**: Two compounding gaps meant a whale could park their entire hoard in; gamba_stakes (the 8 game tokens GAMBIT / CROWN / VEIN / PIP / EDGE /; ACE / NOIR / CHERRY plus accrued pending GBC yield) and never pay (`bc996ffd`) + +### Framework +- **Wire anti_alt into ensure_registered (last dead V3 module)**: After the ',profile command not found' incident I audited every; V3 file for actual wire-up. Findings:; - 6 cogs never loaded -- fixed in 860fdda (`dfec0af4`) +- **Load the V3 cogs -- they were never registered**: User reported: ',profile' returns "command not found." Root cause:; the six V3 cog files (cogs/profile, cogs/mastery, cogs/clan_wars,; cogs/inbox, cogs/apex_events, cogs/onboarding) all exist, import (`860fddaf`) +- **V3 Pillar 1: core/framework/render.py Pillow renderer framework**: Adds a unified PNG renderer scaffold so every system can ship a PNG; without copy-pasting the chess board or equalizer chart scaffolds.; core/framework/render_primitives.py: low-level PIL primitives (font cache, (`d9303cbe`) + +### Database +- **Migration 0251 was validating against legacy log rows**: Symptoms: bot crash-looping every restart on; ``CheckViolationError: check constraint; wealth_redistribution_log_kind_check ... is violated by some row``. (`67ace766`) +- **Fix UBI=0 + all 13 stones in NW/drain**: Two compounding bugs explain "0 UBI paid + Gini hasn't moved":; 1. last_activity was NEVER written anywhere in the codebase.; The users.last_activity column has no DEFAULT in the schema, (`c72a0859`) + +### API Changes +- **V3 wrap: API v3 router + anti-alt signals + indices pass**: api/v2/routers/v3.py: consolidated FastAPI router mounted under; /api/v2/v3 with thin reads against every V3 service layer plus; PNG render endpoints (profile, mastery, war, cwe_controller) so the (`0aac10b5`) + +### New Features +- **Add ,delve respec + clearer stat-point explanation in upgrade panel**: New ,delve respec (aliases restat / reset_points) refunds every spent delve stat point for a USD fee. Cost doubles per respec on the same delver ($10k -> $20k -> $40k -> ...) mirroring the ,buddy respec curve. Refused mid-run, charges wallet+bank atomically, rebuilds hp_max from class base so the player's HP snaps back to baseline. New stat_respecs_used column on user_dungeon (migration 0236) plus a non-negative CHECK constraint.; The empty-form ,delve upgrade panel now explains how the point system actually works: 1 point per level, shared pool across all four lanes, allocations are sticky across class rerolls / gear swaps / run resets, only ,delve respec refunds them (with the live current price quoted). Per-point payoffs are spelled out with the secondary effects (SPD -> crit + first-strike, INT -> spell damage for Mage / Druid). ,delve help gets a dedicated Class + stats section so the four related commands are discoverable together. (`01c9eddd`) + +### Reverts +- **Bring back spinner-then-chunked UX, restore token-count footer**: User feedback: live SSE streaming feels noticeably WORSE than the previous; flow. Each OpenRouter delta is typically 2-3 chars, and Discord's per-; message edit throttle (~0.85s) caps visible updates to ~2-3 chars per (`b947af63`) + +### Performance +- **Four more disco chat wins -- fast-path, parallel tools, streamed usage, prefetched history**: Building on the previous concurrency / timeout fixes, this turn adds the; next tier of wins:; 1. cogs/help.py: _select_tool_schemas now returns an EMPTY list when (`ef46d75d`) +- **Unbottleneck disco chat -- raise concurrency, real-stream final pass, longer timeouts**: The "AI timed out" / "connection died" failures users were seeing came from; three overlapping problems:; 1. core/framework/ai/client.py: _MAX_CONCURRENT_REQUESTS was 8 across the entire (`42db5eaf`) + +--- + +## [main] -- 2026-05-12 + +### New Features +- **Buddy Arena Map: branching 14-zone journey across 3 themed regions, capped by a 4-round Champion Tournament**: Player request: "expand more into buddy arenas and buddy battles, maybe with a png map of different battle zones which u can travel along a map to reach the main tournament, kind of like Pokemon Tournament for the GameCube lol". The flat-queue arena (the old `,buddy battle` going to a random tier-matched AI in a void) now sits next to a metagame: every player travels a directed-graph map starting at **Plains Gate**, picks paths through **Verdant Plains -> Stoneheart Pass -> Tideway Coast**, clears region bosses to unlock the next, and finally enters **Champion Hall** for a quarterfinal -> semifinal -> final -> championship bracket. Two hidden side zones (**Ember Grove**, **Moonlit Pool**) shortcut the path when triggers fire (Plains clear / Sharp Eye mastery). New schema: `cc_buddy_map_progress` (cursor + cleared zones + tournament state + champion_count), `cc_buddy_zone_trophies` (per-zone best-score ledger), `cc_buddy_tournament_runs` (history). Travel cooldown 30s, zone-battle cooldown 60s -- both DB-clock-based per CLAUDE.md. New commands: `,arena` (PNG map), `,arena travel `, `,arena battle`, `,arena boss`, `,arena items`, `,tourney`, `,tourney start`, `,tourney fight`. Map graph + zone data + tournament bracket all in `buddies_config.ARENA_ZONES` / `ARENA_REGIONS` / `TOURNAMENT_BRACKET`; state machine in `services/buddy_arena_map.py`; cog is `cogs/buddy_arena.py` so cogs/buddy.py stays untouched. +- **Battle consumables: 8 craftable Pokemon-style items used via in-battle drop-down with per-round cooldowns**: Player request: "pls craftables for pets, and consumables used with a drop-down in-battle (with round per round CD like pokemon)". Eight items now live in `buddies_config.BATTLE_CONSUMABLES`: **Quick Berry** (25% HP heal, CD 3r), **Focus Berry** (next attack guaranteed crit, CD 4r), **Vial of Rage** (+30% ATK 2 rounds, CD 5r), **Iron Vial** (-25% dmg taken 2 rounds, CD 5r), **Swift Dust** (+0.30 SPD permanent for the battle, CD 4r), **Cure Balm** (clears debuffs + 10% HP, CD 3r, rare), **Shock Bolt** (0.60x ATK ignore-defence + stun, CD 6r, rare), **Phoenix Tear** (revive at 35% HP once per battle, epic). All eight have matching crafting recipes in `crafting_config.CRAFT_ITEMS` (alchemy / tinkering / enchanting specialties at min_level 2-28). New `battle/` apply route in `services/crafting.py` deposits into `user_buddy_economy.battle_inventory` JSONB, mirroring the bait_inventory / fertilizer_inventory pattern. The battle view in `cogs/buddy_arena.py` reads the inventory, builds a `discord.ui.Select` of up to 25 options (greyed when on CD), and the per-round CD tick decrements every round via `services/buddy_consumables.tick_cd`. Mastery's **Quickdraw** node trims 1 round off every consumable CD. +- **True FPS battle animations: 6-frame attack bursts at ~2.2 FPS via Discord message edits during zone / tournament battles**: Player request: "pls more animation, pls more png, pls more pillow ... do not forget animations and ASCII, and many many frames and pngs, that is extremely important." Every attack and consumable use during a zone or tournament battle plays a 6-frame burst sequence (prepare -> strike-arc -> impact-flash -> recoil -> settle -> final) by rapidly editing the message attachment. Frame interval is 0.45s (~2.2 FPS) configurable via `buddies_config.BATTLE_FRAME_INTERVAL_S`. A per-battle `asyncio.Lock` plus a global `asyncio.Semaphore(4)` cap concurrent FPS work so a busy guild can't stampede Discord's edit ceiling; battles also hard-cap at `BATTLE_MAX_BURSTS_PER_BATTLE = 30` so a long fight can't blow past 180 edits. The base scene is rendered by `services/buddy_battle_scene.render_battle_frame` (1200x620 Pillow PNG with two procedural buddy portraits, zone-themed background silhouettes per region, HP bars, status pills, action banner, round indicator); the per-frame overlay (`render_attack_burst`) composes motion lines / impact flashes / recoil tints on top. Rate-limit failures degrade gracefully -- the frame is skipped and the final settled scene still lands. +- **Procedural Pillow buddy portraits: 12 species rendered from scratch with mood + battle-pose variations**: Player request: "expand the current buddy embed (with animations ASCII) with pillow pngs hehe (full fleshed)". New `services/buddy_portrait.py` builds a 480x480 PNG per buddy entirely from Pillow primitives -- no external sprite assets ship; each species gets a hand-keyed style (body color, accent, eye color, shape, ear shape, tail / feature) so a Zenny (yellow parrot, oval body, beak) and a Wolf (grey, tall, triangle ears) read distinctly. Mood / battle poses (happy / neutral / hungry / sad / eating / petted / talking / attack / hurt / victory / down / using_item) tweak eye geometry, mouth shape, action-overlay (swooshes for attack, star burst for victory, impact stars for hurt, item sparkle for using_item). 6-frame attack-burst variant (`render_attack_burst_portrait`) overlays motion arcs / impact flashes / recoil shake / dust puffs / settle. Per-process LRU cache (64 entries, keyed by species + mood + level/10 bucket + gear-hash + theme + size) so back-to-back renders of the same buddy stay cheap. Portrait is composed into the battle scene by `services/buddy_battle_scene` (left-facing player, mirrored opponent) so battle scenes show both portraits side-by-side. +- **Expanded ASCII frames: 5 new battle poses (attack / hurt / victory / down / using_item) across every species**: Player request: "you may also expand or edit the current ASCII to be better". `buddies_config.BATTLE_FRAMES_GENERIC` ships fallback ASCII for the five new poses (attack frame shows ">>" motion lines and a "X" mouth, victory frame shows stars and "Y" arms up, etc.) so every species inherits them without having to ship 12 species x 5 = 60 hand-drawn frames. The new `battle_frame(species, frame_key)` helper resolves species-specific overrides first, then falls back to the generic dict, then to the species' neutral frame -- so species can override individual poses in their own `SPECIES[*]['frames']` dict without affecting the rest. The buddy panel's existing 4s LiveEngine tick continues to use the original mood frames; the new ASCII poses surface in battle log frames where the action context calls for them. +- **Arena Map PNG renderer: 1200x900 hand-laid map with current/cleared/locked/hidden nodes, edges, region bands, and tournament pill**: New `services/buddy_arena_render.py` produces the `,arena` map image. Each zone is a 28-34px disc on a hand-positioned coordinate grid grouped by region band; edges thicken-and-tint based on whether either endpoint is current (cyan), cleared (gold), region-locked (charcoal), or normal-travelable (slate). Boss zones get a yellow 5-point star. Hidden zones render as a dotted disc with a question mark until their unlock fires. A pulse halo wraps the current zone, a check-token stamps on cleared zones, and tier-min/max labels sit under every name. Top-right corner shows the tournament status pill (LOCKED / READY / IN PROGRESS / CHAMPION xN). Companion `render_tournament_bracket` renders a 4-column bracket PNG with the active round highlighted. +- **Apex Mastery: 18 new nodes (38 total) for Buddy Battles + cross-system progression**: Player request: "as an extra, pls make more mastery unlocks for various other systems and edit the mastery PNG to support!" New nodes added to all four branches: **Economy** adds Vault Interest (+8% bank APR), Loyal Customer (-5% shop prices, clamped at -50%), Overtime Hustle (+10% work payouts), Block Royalties (+12% validator share). **Combat** adds Pack Leader II (stacking +10% buddy ATK), Arena Veteran (+25% arena/tourney XP), Quickdraw (-1 consumable round CD), Trailblazer (skip one map hop per travel), Wingbuddy (+1 battle slot). **Luck** adds Spoils of War (+15% zone consumable drops), Twin Forge (10% double craft output), Golden Hatch (+8% egg-rarity upshift). **Utility** adds Hearty Meals (+20% feed efficiency), Light Touch (-15% wallet transfer fees), Heavy Pockets (+20% expedition loot), Charisma Bonus (+15% chat XP), Slipstream Trades (-20% trade gas), Warm Nest (-15% daycare hatch time). Mastery board PNG canvas resized 1200x900 -> 1200x1280 with tighter card heights (132 -> 90) and a new branch-totals legend row so all 38 nodes fit without overflow. +- **Mastery effect consumers wired into earn / bank / buddy_arena (data-only effect_keys now actually move gameplay numbers)**: Earlier work added 20 mastery nodes but `effect_key` values were data-only -- no cog read them, so unlocking a node like Reliable Returns I did nothing in practice. New `services/mastery.apply_passive(db, uid, gid, key, base, mode="mul"|"add"|"cut"|"flat")` and sync sibling `apply_to_base` collapse the read-passive -> apply -> multiply pattern into one call. Wired into `cogs/earn.py` (econ.daily_bonus on the daily payout, econ.work_bonus on the work payout), `cogs/bank.py` (econ.bank_yield + econ.interest_bonus stacked into the savings interest tick alongside existing job / stone / guild multipliers), and `cogs/buddy_arena.py` (combat.buddy_dmg multiplies the player's Fighter ATK at battle start; luck.zone_drops bonus on item-drop rolls after zone clears; combat.zone_travel grants a one-hop skip; combat.consumable_cd trims battle item CDs). Each call site is single-line + best-effort wrapped so a passive read failure never blocks the underlying action. + +### Changes +- **Crafting `apply` accepts a new `battle/` prefix that routes to `user_buddy_economy.battle_inventory`**: Added alongside the existing `bait/` `fert/` `consum/` `buddy/` `cosmetic/` `weapon/` `armor/` routes in `services/crafting.py`. Each `,craft apply ` deposits the count into the JSONB inventory column the buddy battle dropdown reads; no schema for "buddy battle inventory" beyond a single JSONB column on the existing `user_buddy_economy` row (mirrors how fishing's bait_inventory and farming's fertilizer_inventory live). New migration 0252 adds the column idempotently along with the map progress / trophies / tournament tables. +- **Buddy battle engine exposes a stepwise `StepBattle` wrapper alongside the legacy `run_battle`**: `services.buddy_battle.run_battle` keeps simulating a full battle synchronously (used by the existing PvP / arena flows so their behaviour is unchanged). For the new map / tournament view that needs to interject between rounds (player picks a consumable, then the round runs), a thin `StepBattle` dataclass + `step_round(b)` function shares the same `_attack`, `_tick_poison`, `_tick_regen`, `_tick_overclock` helpers so the math doesn't fork. `finalize_step_battle(b)` resolves the BattleResult at end-of-fight. The cog calls `services.buddy_consumables.apply` between rounds to mutate the Fighter state (HP heal, ATK buff, def buff, crit-next flag, stun, revive flag) and `tick_cd` to advance the per-item round counters. + +### New Features +- **`,profile shop` is now button + dropdown driven instead of page-number commands**: Player report: "profile shop should have button or dropdown navigation, profile shop 1, 2 is absolutely horrible way to navigate". The old flow forced players to type `,profile shop` then `,profile shop 2` then `,profile shop cats 2` to walk pages or change themes -- one keystroke-heavy command per page flip with no way back. Now `,profile shop` opens a persistent view with Prev / Next page buttons, a center Page-X-of-Y label, and a theme dropdown that lists "All themes" plus every individual theme. Hitting Next or picking a theme edits the message in place: the PNG re-renders, the wallet readout refreshes, and button states (disabled on first/last page) update automatically. View is locked to the original invoker (same pattern as `,tour` and `,mastery` views) and times out after 3 minutes of inactivity. The PNG subtitle no longer prints "Next: `,profile shop X N+1`" because the buttons replace that instruction. `,profile shop ` still works as a starting filter, but the trailing page-number argument is gone -- the view drives all navigation. +- **`,tour` deck expanded from 5 to 9 cards with deeper copy and a richer PNG**: Player report: ",tour is slim asf" -- the old deck had 5 cards (Wallet / Earn / Trade / Buddy / Mastery) at 4 lines each, no blurb under the title, and the PNG was 1000x600 with one panel. Now: 9 cards covering Wallet, Earn, Trade, Mastery, World Events, Buddy, PvP / Exploit, Items / Shop, and a "Where to look next" card. Each card has a one-sentence framing blurb shown both in the embed and on a dedicated panel at the top of a larger 1200x720 PNG, a section-coloured halo behind the title so the branch reads at a glance, and 6 to 8 body lines (up from 4). The embed now also shows the card number ("Card 5 of 9") so a player can pace themselves. Done state lists nine actions instead of five. +- **`,tour` rebranded from "Apex Onboarding" to "Discoin Tour"**: Player report: "why does it say 'welcome to apex' where TF did u get apex as the game name from this is Discoin". "Apex" is an internal codename for the V3 update; it was leaking into every onboarding embed title, footer, and the done-card subtitle. Replaced all player-facing strings: "Apex Onboarding" -> "Discoin Tour", "welcome to Apex" -> "welcome to Discoin", and the final card's "Apex onboarding complete" -> "Discoin tour complete". The skipped-state embed now correctly tells players how to resume (`,tour` / `,onboard` / `,newplayer`). +- **`,mastery` board embed now actually explains what mastery is**: Player report: "wtf even is mastery, it hardly says any info". The main `,mastery` embed used to be one line summarising points + node count. Now adds three richer fields: **Branches** (Economy 2/5, Combat 1/5, Luck 0/5, Utility 0/5 -- one line, all four), **Top tracks** (your three highest-level tracks with XP-to-next), and **Suggested next unlocks** (up to 4 affordable, unblocked nodes with cost + effect description). Footer points at the new subcommands. Title rebranded from "Apex Mastery" to "Mastery - cross-system progression" for the same reason as the tour rebrand. +- **`,mastery tracks` -- list all 9 tracks with where each one earns XP**: Brand new subcommand. For every track shows the emoji + label, a one-line description of which minigame feeds it (`,fish` -> Fisher, `,delve` -> Delver, etc.), the synergy node call-out (which branch passive double-dips), and your current XP progress. Centralises the XP-source map in `mastery_config.TRACKS` so the data has one home. +- **`,mastery branches` -- explain the four branches of the node tree**: Brand new subcommand. Each branch (Economy / Combat / Luck / Utility) gets a tagline, a "what it does" paragraph, your unlock progress in that branch (X/5), the total points needed to fully clear it, and the full list of nodes with cost + description. Lets a new player pick a spec before sinking points blind. +- **`,mastery info ` now shows the full prereq tree and what the node unlocks**: Old version listed only direct prereqs. Now also surfaces the forward links ("Unlocks: `combat.dungeon_dmg.2` -- Sharp Edge II"), uses the branch's accent colour for the embed, includes the branch emoji + label, and the footer prints the unlock command so a player can copy-paste straight to `,mastery unlock`. +- **`,apex catalog` -- browse every world event in the rotation**: Brand new subcommand. Lists all 7 events sorted rarest-first by weight, with each entry showing the rarity tier, duration, pick-rate percentage (computed from `weight / total_weight`), and the first three modifiers in shorthand. Before this command players had no way to learn what the rotation contained without admin-only `,apex trigger` testing. +- **`,apex info ` -- inspect a single event before it ever runs**: Brand new subcommand. Renders the event poster PNG plus a full embed with flavour, rarity tier + tagline, exact duration, roll weight, and every modifier as `key x1.50 (+50%)` lines that translate the multiplier to a plain-English percent change. Works for any event id whether or not it's currently active. +- **`,apex` idle state now explains what World Events are and how rolls work**: Player report: "wtf even is apex, hardly says any info". The old empty-state embed was "No Apex Events are active. Next roll within 30s." -- functionally a dead end. Now describes that World Events are server-wide buff/debuff windows, lists the exact roll cadence (one roll every `APEX_EVENT_TICK` seconds, `APEX_EVENT_ROLL_PCT` chance) and rotation size (7 events on a weighted draw), names 5 example events, and points at `,apex catalog` / `,apex info` / `,apex history`. Rebranded user-facing strings from "Apex Events" to "World Events" so it's clear this is a Discoin feature, not a separate game. +- **`,apex` live-event view now lists every modifier in plain English**: When an event is live the embed used to show just the name and a poster image. Now adds a structured fields layout: rarity tier with tagline, ends-at as a Discord relative timestamp + clock time, the full modifiers list rendered as `key x1.15 (+15%)` so a player can read what's actually changing without having to interpret a bare `x1.15`. The "Also active" overflow line still works when multiple events stack. + +### New Features +- **Cosmetic sigils are now real procedurally-drawn art instead of "first letter in a colored circle"**: Player report: "the sigils, frames, etc all just colors? where the fuck are the pixel art banners and such you promised? where is the actual shop where it shows the real items u get? so I can see the actual sigils before I buy them? I mean I just bought a cat sigil it's just a fuckin circle with a C in it, that is clearly not a fucking cat." -- entirely correct read. New `core/framework/sigil_art.py` mirrors the existing `core/framework/banner_patterns.py` and ships per-id procedural draw functions for every themed sigil and every legendary sigil: **cat** (head ellipse + triangular ears + eye dots + whiskers + nose), **moon** (disc + bitten crescent + corner stars), **turtle** (hexagonal shell with radial scute lines + head poking out + flippers), **five_star** (proper 5-pointed polygon, used by `star` and `star_shop`), **tidewave** (three stacked sine waves + crest highlight), **cross_bones** (skull with eye sockets + jaw + crossed bone polygons with rounded end-caps), **dice** (rounded square face + 5-pip pattern), **gavel** (mallet + diagonal handle + base block), **cards** (heart/diamond/spade/club mini-suits in a 2x2 grid), **eagle** (heraldic wings + body silhouette), plus mastery-track sigils (anchor, leaf, sword, paw, shield, lightning, flame, wave, snowflake, crown, chart, hammer, scale) and three legendary ones (**phoenix** = eagle + flame plumes, **dragon** = coiled S-body with spike crest + horn dot, **infinity** = double rings + sparkle). Each sigil renders as a darker disc backdrop + highlight ring + the silhouette in the player's chosen sigil colour, so it pops off the banner background. Unthemed / unknown sigil ids silently fall through to the legacy glyph-in-disc path so nothing breaks. Wired into `services/profile_render`, `services/level_render`, and `services/payout_render` so `,profile`, `,level`, and every payout receipt (`,daily` / `,work` / `,ape` / `,beg`) all pick up the new art at once. +- **Cosmetic frames now carry actual flair around the avatar instead of a plain coloured ring**: New `core/framework/frame_art.py` layers per-frame decorations on top of the existing avatar disc + ring. **Tabby** gets four short claw-mark arcs at the diagonals; **crescent** hangs a small moon at NE; **shell** / **coral** stud the rim with six evenly-spaced bumps; **comet** draws a bright head with a dotted trail curving along the upper arc; **anchor_chain** paints twelve alternating chain links around the rim; **cards** drops four suit pips at N/E/S/W; **eagle** wreaths the bottom with two laurel sprigs; **diamond** facets the rim with eight triangle teeth alternating between primary and accent; **obsidian_ring** stacks three concentric outlines for depth; **ember** and **frost** halo the disc with a gaussian-blurred glow (drawn UNDER the avatar so the avatar paints over the inner glow). Plain colour rings still render for the basic frames (`simple`, `gold`, etc.) -- the decorator is purely additive. +- **Cosmetic shop now shows the ACTUAL art for every item, not a colour swatch**: Each shop card now reserves a 96x96 art well that renders the real cosmetic preview: sigils render the same silhouette the player will see on their profile, frames render a placeholder avatar disc with the ring + decorator (so you see the claw marks / halo / chain BEFORE you buy), titles render as a coloured ribbon badge with the title text and the first 32 characters of the epithet, and banners render the procedural pattern (stars / moon / sun / pirate ship / cats / cards / etc.) onto a tile coloured with the actual banner background. The card itself grew from 270x110 to 270x150 to fit the art well plus a stacked name / id / price / rarity column on the right. Solves the "I just bought a cat sigil it's a fucking circle with a C in it" problem for every themed cosmetic on the shop floor. +- **Equipped titles now MEAN something: lore epithet line + gameplay passive + achievement-gated obtainment**: Player report: "MAKE THE TITLES AND SHIT MEAN SOMETHING. THIS IS COOL BUT MEANINGLESS. GIVE IT MEANING." Every title in `cosmetics_config.TITLES` gained three weight-bearing fields. **Epithet**: a one-line flavour quote rendered under the player's name on every profile / level / payout card. "Cat Lord" now reads `Nine lives, zero regrets, full collar.`, "High Roller" reads `You don't fold. You levitate.`, "Mythical" reads `Half the server thinks you're a server alt.`. **Passive effect**: each title now carries an `effect_key` + `effect_value` pair on the same dotted-namespace mastery nodes use (`combat.gamba_payout`, `econ.lp_yield_bonus`, `utility.crafting_speed`, etc.). New `services/cosmetics.title_passives(db, uid)` returns the same `{effect_key: value}` dict shape `services/mastery.passives` returns, so any consumer can merge the two and read the union via the existing `mastery.apply` helper without learning about titles specifically -- a player with the Pack Leader node + Cat Lord title now stacks `combat.buddy_dmg` from both sources. **Achievement-gating**: three themed titles bind directly to existing firing achievement badges -- `kitten` -> `new_best_friend` (first buddy adopted), `cat_lord` -> `buddy_champion` (25 buddy battles won), `captain` -> `robin_hood` (10 exploit raids won) -- and a new auto-grant hook in `services/achievements.grant()` calls `services/cosmetics.cosmetics_for_achievement()` so the title lands in the player's inventory the moment they earn the badge, no manual claim. The other themed titles stay shop-bought for now (the achievement triggers they'd want -- savings streak, vault L5, gamba session, governance vote -- aren't wired yet); pointing those to badge ids in cosmetics_config is the only change needed when the trigger lands. + +### New Features +- **Every minigame micro-action now nudges its mastery track up, not just the cashout**: Player report: "why the fuck aren't fishing mastery and craft mastery and shit leveling ?? cmon dude what the fuck make this uniform and clean". The previous design only granted track XP at the end-of-loop cashout (delve cashout / fish reel cashout / craft forge cashout). A player who fished for an hour but hadn't cashed out yet showed no fisher progress; ditto for crafting and farming. Raider levelled because `,exploit` is itself the cashout event, so its track was the only one that ever moved without an explicit cashout. New `services.mastery.attach_listeners(bot)` wires onto the same event bus the achievement engine uses and grants a small flat XP per micro-action: `fish_caught` -> fisher +5 (legendary +25), `farm_harvest` -> farmer +5, `craft_made` -> crafter +5, `delve_kill` -> delver +5 (boss +25, full clear +15), `trade_executed` / `swap_executed` -> trader +3, `gamble_win` -> gambler +5, `buddy_battle_win` -> tamer +5 (arena +10, adopt +10), `exploit_win` -> raider +5, `stake_created` / `validator_registered` -> validator +5/+25. Cashout still does the big USD-scaled grant via `xp_for_action` -- the micro-action grants are the "fishing-in-the-background feels alive" baseline. Wired from `cogs/mastery.py::Mastery.cog_load` so the listener attaches at bot boot. A new test in `tests/test_mastery.py::test_attach_listeners_subscribes_to_micro_action_events` pins the dispatch table and asserts every one of the 9 tracks has at least one event feeding it, so no track can quietly go silent again. + +### Changes +- **`,daily` / `,work` / `,ape` / `,beg` receipt cards no longer overlap themselves AND no longer show a phantom "Lv 1"**: Player report: "clearly beg, ape and work aren't being shown the same information and the png for those three look like shit. also, why is it saying I'm a level 1 airdrop farmer when I most certainly am not??" Four separate visual bugs fixed in one pass on `services/payout_render.py`. (1) The "L1" / "LOSS" / "WIN" badge sat at `(200, 110)` -- exactly on the avatar's right edge -- and decorated frames (tabby claws, cards pips, dice pips, shell bumps) painted ornaments past x=200, so the badge always sat ON the frame ornament. Badge moved to `(180, 142)` and shrunk from diameter 44 / font 14 to 36 / 12 so it tucks cleanly past the disc and ornament. (2) The avatar disc at size 140 reached down to y=250 but the big NET REWARD pill spans y=220-290; the pill was slicing the avatar's bottom edge on every card. Avatar shrunk to size 100 so it ends at y=210 -- pill clears it with a 10 px gap. (3) Every payout subtitle read "{Job} - Level 1" because the code did `job.get('level', 1)` and the `user_jobs` table has no `level` column, so the fallback `1` fired for every player regardless of work_count. Subtitle and badge now use the real `work_count` from the DB: "Airdrop Farmer - Session 50" + badge "S50". (4) `,ape` losses showed three columns of `$0.00` because gross / tax / bonus are all $0 on a forfeit -- wasted real estate. Zero-value stat blocks are now skipped and the remaining visible blocks expand to fill the strip evenly so a one-stat card looks intentional, not broken. +- **Profile MASTERY field sums every track, not just the seeded ones**: A player with one big-mastery track and the rest untouched saw `L27 sum` instead of `L35 sum` because untouched tracks weren't in the `mastery_summary["tracks"]` dict at all and were therefore counted as L0. The DB column defaults to L1 once a row is seeded, so the renderer now treats every missing track-row as L1 (iterating over the full `mastery_config.TRACKS` key set) so the displayed sum matches the actual baseline. +- **Mastery level curve steepened so mid-tier takes real commitment, not one play session**: Player report: "make it take a while to gain mastery, make it worth while, that seems like they can get max level in like a week". Followup tune on top of the per-action XP rebalance from the same commit window. Bumped `mastery_config.TRACK_BASE_XP` from 100 to 300 and `TRACK_XP_GROWTH` from 1.15 to 1.22 so the same XP grants now buy fewer levels. Effective milestones under the new curve (cap is 1500 XP per capped action): L5 ~ 1,656 XP (1 capped action), L10 ~ 6,797 (4), L20 ~ 58,267 (38), L27 ~ 238,537 (159), L50 ~ 23.2M (way beyond a single session), L100 ~ 483B (intentionally unreachable on human time). A casual player doing ~5 actions at $500-$2000 each per day lands at L5-L10 over the first couple weeks; an active player doing 20 actions a day hits L20 in ~2 weeks and L27 in ~2 months; a whale soaking the cap on 50+ actions/day still needs ~10 months to reach L50. Points per level (`L-1 + L//10`) is unchanged -- the slowdown of XP gain is what makes the points feel earned. Two new pinned regression tests in `tests/test_mastery.py` lock the L5 / L10 / L20 / L27 / L50 thresholds so the curve can't quietly soften again. +- **Mastery XP per action centralised + retuned so a single big payout can't single-shot mid-tier**: Player report: "I did one delve battle and am immediately raider lvl 20 ... Raider L27 I don't even know what the hell that is, I've hardly delved at all and I'm level 27 please make the XP make sense??? I have 28 points available for mastery and I could literally buy everything, played the game once". Every minigame end-of-action cog (delve cashout / gamba cashout / farm harvest / craft cashout / fish cashout / exploit raid) was using its own `max(N, int(to_human(usd) / 10.0))` (raider was twice as generous at `/5.0`) so a $50k Full Protocol Heist produced 10,000 XP -- past L20 in ONE action. New `services/mastery.xp_for_action(usd, *, multiplier=1.0)` lives in one place and applies `usd / 50.0` capped at 1500 XP per action. Same formula for every track now (raider's old 2x bonus is gone since the player explicitly hit the curve there), with a `multiplier` knob for any track that needs a small skew later. Curve at a glance: $500 cashout = 10 XP, $5k = 100 XP, $50k = 1000 XP, $100k+ = 1500 XP (capped). The level curve itself is unchanged (L10 ~ 1,675 XP, L20 ~ 8.8k, L50 ~ 627k) so a player who plays a few sessions now lands at L3-L6 instead of L27. Six new pinned regression tests in `tests/test_mastery.py::test_xp_for_action_*` guard the curve so the divisor can't drift back. + +### Bug Fixes +- **Profile / payout / shop cards no longer overlap their own elements**: Visual audit pass. **Profile**: long display_names like `lleywyn_a_very_long_display_name_to_test_overflow` spilled past the right edge of the card (the previous `[:40]` char slice didn't measure pixels); new `_fit_to_width` helper truncates with an ellipsis based on `ImageDraw.textlength` at the actual rendered size. The 8-pair PLAYER DETAILS grid clipped the bottom row against the panel edge (4 rows of 28 px = 112 px in a 104 px interior); tightened row spacing to 23 px and bumped the top edge up 2 px so all 8 pairs fit cleanly. **Payout**: the title epithet was being rendered at `(240, 80)` on every `,daily` / `,work` / `,ape` / `,beg` receipt, where it crashed into the L-badge zone and the avatar disc; dropped from the payout flow entirely (the title label is already in the subtitle line and the small card has no room for a second-row quote without overlap). The full epithet stays on the larger `,profile` + `,level` cards. **Shop**: title cards rendered on a saturated yellow background that fought the ribbon inside the art well, and the epithet drawn inside the well bled out the right edge into the name column. Title cards now sit on the same neutral dark backdrop the player will actually see in-game; the ribbon is centred in the art well with pixel-truncated label; and the epithet moved to the right column under the id where the column width is enforced by a `_fit_to_width` pass. Names and ids on every shop card are now pixel-truncated too so long entries can't pile onto the price pill. +- **Apex events background tick no longer crashes when an event expires**: Production log: `asyncpg.exceptions.DataError: invalid input for query argument $3: 1778531422.799661 (expected a datetime.date or datetime.datetime instance, got 'float')` raised on every 30-second tick the moment an event's window closed. Root cause: `services/apex_events.expire_finished` was `DELETE ... RETURNING started_at, ends_at` (asyncpg reads TIMESTAMPTZ, `core.database._coerce` turns it into an epoch float for the convention every renderer relies on), then immediately tried to INSERT those floats into the history table's TIMESTAMPTZ columns -- asyncpg's writer can't re-encode a float as TIMESTAMPTZ. Rewrote the function as a single CTE (`WITH expired AS (DELETE ... RETURNING ...) INSERT INTO history SELECT ... FROM expired RETURNING 1`) so the timestamps never round-trip through Python at all. Bonus: the move is now atomic (no risk of partial state if the tick dies between DELETE and INSERT) and is one round-trip instead of N+1. Matches CLAUDE.md's "DB-side clocks for time comparisons" guidance -- same idea, keep timestamp-typed values on the DB side end-to-end. +- **`,work` no longer crashes with `cannot access local variable 'C_AMBER' where it is not associated with a value`**: Player report: ",work with a new banner color says: cannot access local variable 'C_AMBER' where it is not associated with a value". Root cause: `cogs/earn.py:1343` re-imported `C_AMBER` and `C_PURPLE` inside the cosmetic-render try block even though both names are already imported at the module level (line 25). Once Python sees a `from ... import C_AMBER` in a function it treats `C_AMBER` as a local variable in the entire function -- which means any exception that fires BEFORE that import line completes leaves the local unbound, and the next reference (`accent_color=C_AMBER`) raises `UnboundLocalError`. Deleted the redundant import; the module-level names cover the same call sites and the bug-path goes away. Two-line change. +- **`,tour` Next Card no longer jumps from card 1 to the end of the deck on a rerun**: Player report: ",tour 'Next Card' goes immediately from 1 to 9 lol". The Next button delegated to `services/onboarding_deck.advance()`, which re-read `deck_progress` from the DB and bumped it. After a previous completion the `get_progress` helper short-circuits to `len(DECK)` (because `completed_at` is set), so `,tour` would override the display to card 1 but the very next button press would read the stale "all done" state out of the DB and skip straight to the completion card. Fix moves the navigation cursor into `_DeckView.current_idx` so a Next press always advances exactly one card past whatever's on screen, and renames the DB helper to `set_progress(uid, idx)` which takes an explicit index (no re-reading required). Reruns now walk the whole deck again from card 1, and partial progress mid-rerun is persisted so a player who steps away resumes on the correct card. +- **`,apex` no longer crashes with `'float' object has no attribute 'tzinfo'`**: All three live-event surfaces (the `,apex` headline, the `,apex history` list, and the world-event poster PNG) were treating `started_at` / `ends_at` as `datetime` objects, but `core.database._coerce` converts every TIMESTAMPTZ column to an epoch float on the way out of asyncpg. The first attribute access (`.tzinfo`, `.timestamp()`, or `(ended - started).total_seconds()`) blew up before any embed could render. Switched the three call sites to use the existing `core.framework.ui.fmt_rel` / `fmt_ts` helpers (which already accept both floats and datetimes) and taught `services/event_poster._time_progress` / `_format_remaining` to coerce either type through a single `_ends_at_epoch` helper. `,apex trigger` still works because it constructs `started_at` / `ends_at` in Python and the same helper accepts datetimes too. +- **Apex Mastery board node cards now word-wrap descriptions instead of cutting them mid-word**: Player report: "the text is so tiny and bleeds thru the boxes and doesn't show the full thing". The node-tree renderer was slicing each description at a hard 32-char boundary and again at 64, then dropping anything past that -- so "Trim 10% off `,work` / `,daily` / `,fish` cooldowns." rendered as "Trim 10% off `,work` / `,d" + "aily` / `,fish` cooldown" with the period and the close half-word clipped. `_render_node_tree` now uses a pixel-accurate greedy word-wrapper (`_wrap_to_width`) keyed off `ImageDraw.textlength`, falls back to per-character splits for backtick tokens wider than a line, and ellipsises the last visible line when a description exceeds four wrapped lines. Names are likewise truncated with an ellipsis via `_fit_with_ellipsis` instead of a blind `[:22]` cut. Cards widened slightly (165->170) and grew vertically (110->132) to fit the larger 11pt description font, and the title bumped to 12pt so it's actually readable at Discord's default image scale. +- **`,admin reset user` now actually wipes the player's balances**: Player report: ran `,admin reset user @lley`, got the "All data has been wiped" confirmation, then `,bal` came back with $961M net worth still on the card (Safety Module $723M, Disc.Fun $88M, NFTs $61M, Farming $53M, Buddy $26M, Crafting $6M, Delve $748K, Gamba $336K). `database/users.py::reset_user` was deleting from a hard-coded list of tables that hadn't been touched in months, while every V3 economy added its own user-scoped table(s) and was never appended. Reset now deletes from every table that `services/net_worth.py::compute_net_worth` reads: the five primary stones + eight themed/meta stones, `user_fishing` / `fishing_catches`, `user_dungeon` / `dungeon_runs` / `dungeon_kills` / `dungeon_party` (owner_user_id), `user_buddy_economy` / `cc_buddies` (owner_user_id) / `cc_buddy_hatches`, `user_farming` / `farming_harvests` / `farming_pest_battles`, `user_crafting` / `crafting_logs`, `safety_module_stakes`, `discfun_stakes` / `proto_token_holdings` / `proto_token_trades`, `gamba_stakes` / `gamba_chess_stats` / `gamba_checkers_stats` / `gamba_consumables`, `lunar_stakes`, `moon_stakes`, and `nfts` (owner_id). Circulating-supply deduction extended to cover `safety_module_stakes` / `gamba_stakes` / `lunar_stakes` / `moon_stakes` so wiping a whale also pulls their locked tokens out of `crypto_prices.circulating_supply` and `guild_tokens.circulating_supply`. Each per-table delete is wrapped in try/except so a stale DB snapshot missing a newer migration still completes the wipe rather than crashing partway through. The dashboard's `POST /admin/users/{id}/reset` endpoint was carrying a worse copy of the same loop (only wiped wallet/bank/holdings/loans/savings/stakes plus hash/lock/vault stones); it now calls `db.reset_user` so there's one canonical "wipe a user" path the way `services/net_worth.py` is the one canonical "compute a user" path. + +### V3 "Apex" Update +- **Hotfix: rename V3 onboarding to `,tour` -- `,start` is already claimed**: After enabling cog loading in the previous commit the bot crash-looped on `CommandRegistrationError: The command start is already an existing command or alias`. `cogs/overview.py` already registers `,start` (the unified dashboard / game launcher), and the V3 `cogs/onboarding.py` was trying to register the same name. Renamed the V3 onboarding command to `,tour` (with aliases `onboard`, `newplayer`) so both commands coexist: legacy `,start` is still the dashboard, `,tour` is the 5-card V3 onboarding deck. Verified all other V3 hybrid_group names (`,profile`, `,mastery`, `,war`, `,inbox`, `,apex`) and the new aliases are unique across the cogs tree -- nothing else collides. + + +- **V3 wire-up audit pass -- anti-alt detection was dead code**: After the ',profile command not found' incident I audited every V3 file for actual wire-up. Findings: six cogs were never loaded (fixed in the previous commit). The v3 API router is correctly included in `api/v2/main.py`. clan_wars and apex_events background ticks both register heartbeats and `.start()` from `Cog.__init__`. All renderers (mastery / war / inbox / event poster / cwe / payout / level / profile) have at least one caller. **The one remaining gap was `services/anti_alt.py` -- zero callers, completely dead.** Hooked it into `core/framework/middleware.py::ensure_registered` so the first command from a brand-new user fires `twin_join_check`; if another account in the same guild registered within 60s it's flagged as a soft signal in `user_security_signals` (the table the staff dashboard / `,admin security` already reads). Best-effort, no auto-bans, no user-facing change -- purely observational. Dispatch is `asyncio.create_task` so the check doesn't block command flow, and the same first-touch path that fires the welcome DM piggybacks on this one. + + +- **CRITICAL: V3 cogs were never loaded -- `,profile` / `,mastery` / `,war` / `,inbox` returned "command not found"**: The V3 cog files (`cogs/profile.py`, `cogs/mastery.py`, `cogs/clan_wars.py`, `cogs/inbox.py`, `cogs/apex_events.py`, `cogs/onboarding.py`) all import cleanly and expose `setup()` correctly, but they were never added to the `COGS` list in `core/framework/bot.py` -- so `bot.load_extension()` never ran on them and none of the commands or hybrid groups registered. Production logs showed "Loaded 70 cogs" with no V3 entries among them. All six entries appended to the loader (alphabetised, comment-tagged as `# V3 "Apex" Update`). This single oversight is why every V3-Pillar command was invisible at runtime even though the cosmetic shop / shop renderer / profile card / etc. all imported and tested green. + + +- **Fix `,profile` routing -- legacy alias was shadowing the V3 card**: Player feedback: "*can u look at ,profile?? does that even tie into this?*" -- and it didn't, because `cogs/showcase.py` declared `@commands.hybrid_command(name="me", aliases=["showcase", "profile"])` on the legacy 8-tab showcase. That alias collided with the V3 `cogs/profile.py::profile` hybrid_group, and `,profile` was resolving to the showcase Overview tab (which is where "Novice -- DeFi Degen -- Level 1" was actually coming from). The `profile` alias is dropped from `,me`; the legacy showcase still runs as `,me` and `,showcase`. `,profile` now reliably hits the V3 cosmetic-themed PNG card with the equip / unequip / gallery / shop / buy / help subcommands. +- **Profile defaults pass + `,profile help` + show real job + level**: Player feedback: "*Novice -- DeFi Degen -- Level 1*" on the profile card was wrong on two counts -- "Novice" had nothing to do with the game, and the job + level should be shown for real instead of being hardcoded next to a generic title. Three changes: (1) `cosmetics_config.py` gains a true-black `obsidian` banner (color `0x000000`, accent `0xC0C0C0`) and `services/cosmetics.py::equipped()` now defaults the banner slot to **obsidian** instead of midnight blue. (2) The default **title** slot is no longer auto-set to `novice` -- new players start with no title equipped. (3) `services/profile_render.py::render_profile_card()` and `services/level_render.py::render_level_card()` accept the player's job + level (and rank, on the level card) and use them as the subtitle line **whenever no custom title is equipped**. So a fresh player now sees "DeFi Degen -- Level 4" under their name on `,profile`, instead of the old "Novice" placeholder; the moment they equip a shop-bought title (e.g. **Cat Lord**, **High Roller**, **Captain**), the equipped title takes over. New `,profile help` (alias `,profile commands`, `,profile ?`) lists every subcommand with a one-line example -- view, equip / unequip, gallery, shop (and the eight themes), buy -- plus a defaults summary so the player knows where the fallback comes from. The legacy `Novice` cosmetic entry stays in `TITLES` (anyone who manually equipped it keeps it); it's just no longer applied by default. + +- **10/90 coin/token payout split + cosmetic-themed PNG receipts for `,daily`/`,work`/`,ape`/`,beg`**: Pre-V3 each minigame rolled its network coin and yield-token amounts independently, so a single fishing event could mint $6,309 of LURE for $0.07 of REEL once the oracles drifted. V3 ships `core/framework/payout_split.py::rebalance_to_split(db, gid, coin, token, coin_h, token_h)` which reads both oracles, sums the gross USD value, and re-allocates as **10% USD in the network coin + 90% USD in the yield token**. Dropped in at fishing `dig_treasure_map` + `beachcomb` (LURE/REEL) and at crafting's craft-success mint (INGOT/FORGE) -- the helper is generic, so future games just call it before their `update_wallet_holding`. Six new tests cover the lopsided-input bug-shape, fresh USD payout, missing-oracle fallback, zero-input, one-sided coin-only input promoted to 10/90, and the default 10/90 constants. **Pillow receipt cards** -- `services/payout_render.py::render_payout_card()` is a 1200x500 PNG that the `,daily`, `,work`, `,ape`, and `,beg` commands now attach to their reply. The renderer reads the player's equipped cosmetics (banner color + accent, frame ring + width, sigil corner-stamp, title under name) so every payout receipt visually matches the player's `,level` and `,profile` cards -- one identity carried across the whole economy surface. `,daily` carries the streak badge in command-specific color (gold over 30 days, purple over 7, info otherwise). `,work` shows the job-level badge. `,ape` flips the pill green/red on WIN/LOSS with the YOLO entry cost in the subtitle. `,beg` produces a card for every gain tier (SMALL / MEDIUM / JACKPOT) and for the CATASTROPHE loss branch, with the loss percentage in the subtitle. All four cards include the V3 tax + bonus stat blocks, the net-reward pill, the bonus list (wealth curve / job perks / LP / buddy / hall), and the post-claim wallet headline. Every render is wrapped in `try/except` so a render hiccup never blocks the reward flow. + + +- **CWE production hotfix -- UBI stream was failing every payout**: First-day production logs showed `cwe ubi stream: payout failed gid=... uid=...` on every recipient with no diagnostic. Three compounding causes: (1) Migration 0230's `wealth_redistribution_log.kind` CHECK constrained values to `'tax'` / `'ubi'`, but V3 writes per-tx rows with `'tax_tx'` / `'ubi_tx'`. Every insert violated the CHECK, the atomic block rolled back the wallet credit AND the pool decrement, and the helper logged a generic "payout failed". Migration `0251_cwe_kind_check.sql` drops the legacy constraint and adds the expanded one (`'tax'`, `'ubi'`, `'tax_tx'`, `'ubi_tx'`). (2) `Config.COMMUNITY_RESERVE_USER_ID = 0` and the `if exclude_user_id` truthiness guard across `services/net_worth.py` is a no-op when the sentinel value itself is 0, so the protocol reserve leaked back into the eligible recipient list as `uid=0`. Defensive `uid > 0` filter added in `services/cwe.py::_ranked_networth` (covers leaderboard + tax-on-credit + UBI stream paths) plus a belt-and-braces skip in the payout loop. (3) The exception handler swallowed the real cause; now logs `type(exc).__name__: exc` plus `exc_info=True` so future failures are diagnosable from one line. Also surfaces an "overflow guard" in `cogs/trade.py::_rebalance_pools_for_guild` that skips a pool whose arb quadratic produces NUMERIC(36,0)-overflowing reserves instead of crashing the `prices_updated` bus listener -- triggered by V3 Pillar 8 making more swap-events fire the rebalancer on tiny-priced tokens with deep pools. + + +- **Profile cosmetics shop + Pillow `,level` card**: New `,profile shop` (paginated by theme) and `,profile buy ` let players spend USD on themed cosmetic upgrades. Eight themes added to `cosmetics_config.py` with a title + banner + frame + sigil set each: **cats** (Cat Lord / Whisker Dawn / Tabby / Cat sigil), **moons** (Moonchaser / Lunar Glow / Crescent / Moon), **turtles** (Sea Turtle / Shell Sand / Shell / Turtle), **stars** (Star Walker / Stellar / Comet / Five-Star), **ocean** (Tidemaster / Deep Ocean / Coral / Tidewave), **pirates** (Captain / Jolly Roger / Anchor Chain / Cross Bones), **gambling** (High Roller / Vegas Strip / Cards / Dice), **politics** (Senator / Capitol / Eagle / Gavel). 56 new shop items at prices $500-$12,000 plus the pre-existing shop entries -- 57 listings total. `services/cosmetics.py` gains `shop_price_usd` (parses `unlock: "shop:N"` into a float), `shop_listings(theme=...)` (filterable catalogue), and `buy(db, uid, gid, slot, item_id)` (wallet-first-then-bank debit, idempotent ownership grant, full audit-log entry). New `services/profile_render.py::render_shop()` (1200x900 PNG with theme grouping, slot pill, price tag, OWNED badge for items you already have). Pillow-optimized `,level` / `,rank` card via new `services/level_render.py::render_level_card()`: 1200x440 PNG that *reads the player's equipped cosmetics* and accents the card with their banner / frame / sigil / title, so the chat-level view feels of-a-piece with `,profile`. Falls back to the legacy embed if the renderer hits an error. 11 new tests covering price parsing, theme filter, eight requested themes existing, and renderer smoke for shop + level card (themed + default + zero-XP edge case). All 1264 tests pass. +- **V3 wiring batch -- per-cog hooks for CWE tax, mastery XP, exploit clan-war record, gamba Apex Event payout modifier**: First-pass wiring at representative high-volume call sites so the V3 spine is exercised end-to-end. `cogs/earn.py` `,daily` and `,work` payouts now route their gross reward through `services.cwe.tax_on_credit()` so the percentile-based per-tx tax + floor bonus apply on every credit (Pillar 10). Mastery XP emits land at the major cashout boundaries: `,fish cashout` -> fisher track, `,farm cashout` -> farmer, `,delve cashout` -> delver, `,craft cashout` -> crafter, `,gamba cashout` -> gambler. XP grant is 1 point per $10 cashed out (capped at 1 minimum), bus-logged but non-fatal if mastery is unavailable. `cogs/exploit.py` resolution now grants raider mastery XP scaled to the heist amount and, when the attacker is in a group, records the contribution against the **Apex** node of any live clan war (V3 Pillar 3). `services/gamba.py::mint_token_for_win()` now stacks the active `gamba.payout` Apex Event modifier on top of its win multiplier so events like Blood Moon (+15%) materially change game-token mint rates. Every hook is wrapped in `try/except` so a V3 service hiccup never aborts the upstream economy flow. +- **V3 Pillar 1 -- `core/framework/render.py` Pillow renderer framework**: Every PNG-producing surface in the bot used to copy-paste the chess board scaffold or the equalizer chart scaffold. New `core/framework/render.py` (`RenderCanvas`) + `core/framework/render_primitives.py` consolidate PIL into one place with reusable primitives: `title`, `rounded_panel`, `pill_badge`, `progress_bar`, `stat_block`, `divider`, `avatar_circle`, `glyph_token`, `text`, `footer`, `halo`. Colors flow through `constants/ui.py` so a palette change propagates automatically. Fonts cached via `functools.lru_cache` against the bundled `assets/fonts/DejaVuSans*.ttf`. Generic `render_line_chart` + `render_bar_chart` helpers ship out of the box. Smoke benchmark: 1200x600 canvas with 6 stat blocks renders in ~206ms (under the planned 250ms budget). Foundation for V3 Pillars 2-7 + 10. +- **V3 API surface + anti-alt safety signals + indices pass**: New consolidated `api/v2/routers/v3.py` mounts every V3 system on the existing FastAPI app so the frontend can render the same data the bot does. Endpoints under `/api/v2/v3`: `mastery/{uid}`, `mastery/{uid}/unlock`, `cosmetics/{uid}`, `cosmetics/{uid}/equip`, `inbox/{uid}`, `inbox/{uid}/read`, `events/apex/active`, `events/apex/history`, `wars/active`, `wars/{match_id}`, `cwe/controller/{gid}`, `cwe/simulate/{uid}`, plus PNG render endpoints (`render/profile/{uid}.png`, `render/mastery/{uid}.png`, `render/war/{match_id}.png`, `render/cwe_controller/{gid}.png`) so the dashboard can embed the exact PNGs the bot ships. Auth-required mutations use the existing `get_current_user` dependency. New `services/anti_alt.py` ships soft-flag heuristics (`flag_pair`, `twin_join_check`, `unresolved_pairs`, `resolve`) that write into `user_security_signals` (migration `0243_anti_alt_signals.sql`); no auto-bans -- the staff dashboard surfaces flagged pairs and operators decide. Final indices pass migrations `0244_v3_mastery_seed.sql` / `0245_v3_indices.sql` / `0250_v3_indices_phase8_10.sql` add btree indices on every new `(uid, gid)` composite key and the hot read paths for inbox/cosmetics/clan-war contributions, apex_events lookups, CWE controller log, and CWE per-day reset queries. +- **V3 Pillar 7 -- Onboarding deck (`,start`)**: `,help` was 6,241 lines of cog list -- a new player had no path through the V3 surface area. `,start` (aliases `,onboard`, `,tour`) ships an interactive 5-card Pillow PNG deck (1000x600 each): **Wallet** (balance/bank/move/pay), **Earn** (daily/work/faucet + CWE context), **Trade** (buy/sell/swap/chart + Pillar 8 chart-impact callout), **Buddy** (adopt/feed/battle + Tamer-track mastery callout), **Mastery** (the cross-system progression + `,profile` identity callout). Two-button view (Next / Skip) with per-user progress persisted in `user_onboarding` (migration `0242_onboarding_progress.sql`) so a player who steps away can rerun `,start` and pick up where they left off. The done card surfaces the V3 entry points (`,profile`, `,mastery`, `,inbox`, `,apex`) so finishing the deck hands the player straight into the new systems. +- **V3 Pillar 3 -- Clan Wars (Apex Conflict)**: `cogs/groups.py` had 5,059 lines of treasury/perks/halls but zero way for two guilds to test themselves against each other. V3 ships the PvP layer on top. Weekly Apex Conflict cycle: groups `,war queue` to enter the pool, the 60s matchmaking tick pairs them FIFO (NW-similar matching is a future improvement), and each pair fights over a 12-node board (Mine, Vault, Bazaar, Forge, Lighthouse, Reef, Grove, Crypt, Spire, Orchard, Forum, **Apex**) for 7 days. Every economic action ties to a node via a single-line `clan_wars.record(db, gid, uid, group_id, node_id, points)` hook -- mining a block = points to Mine, catching a legendary fish = Reef, auction sale = Bazaar, etc. Apex is the highest-weighted node (x3) and only flips on successful exploit raids against the rival guild. Settlement at the end of the window splits the entry pool 80/20 winner/loser (50/50 on a tie). New `,war` (PNG world-map of the live board with per-node scoreboards + time-remaining strip + winning side highlighted), `,war queue`, `,war history`. PNG renderer (`services/war_render.py`) is a 1600x900 hand-laid map composed against `core/framework/render.py`. Migration `0238_clan_wars.sql` ships `clan_war_matches`, `clan_war_nodes`, `clan_war_contributions`, `clan_war_queue` with appropriate indices. Cross-cog wiring (exploit/mine/auction/craft/dungeon/validators/trade/governance recording into clan_wars.record) ships in the final wiring batch. +- **V3 Pillar 4 -- Profile cosmetics (Title / Banner / Frame / Sigil)**: `,me` showcase tabs showed stats but nothing the player chose -- there was no identity surface. V3 adds four equippable cosmetic slots tied to the user (NOT per-guild -- identity is global): **Title** (short string suffix), **Banner** (background art + accent), **Frame** (avatar ring color/width), **Sigil** (corner emblem). 21 titles, 12 banners, 8 frames, 16 sigils at launch; each item declares an `unlock` source in `cosmetics_config.py` (`mastery:fisher:50`, `season:winner`, `achievement:net_worth_10m`, `shop:7500`, or `system` for the default everyone gets). New `,profile` command renders a Pillow PNG (1200x600) compositing the equipped Banner background, framed avatar, sigil stamp, title text, net-worth headline, mastery roll-up, and optional season-rank + clan-war scoreline + badge row. New `,profile equip ` / `,profile unequip ` / `,profile gallery` (Pillow grid of owned cosmetics). Migration `0239_profile_cosmetics.sql` ships `user_cosmetics_owned` + `user_cosmetics_equipped` (per-user). Achievement / season / mastery grant hooks land in the wiring batch. +- **V3 Pillar 6 -- Apex Events (cross-system world events)**: `cogs/events.py` events used to be market-only -- pump/dump phases that only touched price drift. Apex Events sit one layer above and inject MODIFIERS into multiple disciplines simultaneously. Seven launch events: **Solar Flare** (mining +50%, fishing -20%, dungeon mob damage +25%), **Blood Moon** (raid cooldown half, gamba +15%, savings -10%), **Harvest Bloom** (farming +40%, LP fees waived), **Vault Tremor** (vaults shed 1% but mastery XP triples), **Deep Liquidity Wave** (swap impact -25%, LP yield +25%), **Raider Dawn** (exploit damage doubles, defenders earn double), **Silent Market** (chart drift halved, auction fees -10%). Every event has a stable id, flavour text, duration, rarity (info/warning/volatile/catastrophe drives accent color), modifier dict, and weight. Consumers across the bot call `await apex_events.modifier(db, gid, "mining.hashrate")` and the per-guild cache returns the cumulative multiplier (with a 30s TTL so it's free on the hot path). Background heartbeat (`cogs/apex_events.py`) rolls every 30s at 5% per-guild probability; the same event can't double-fire while live. Each new event broadcasts a Pillow-rendered 1400x700 poster (themed accent, modifier badges, time-remaining bar) into every active player's inbox (V3 Pillar 5) plus the dashboard. Commands: `,apex` (current event poster), `,apex history` (last 10 in the guild), `,apex trigger ` (admin-only). Migration `0241_apex_events.sql` ships `apex_events_active` + `apex_events_history`. The actual modifier-read one-liners in `cogs/chain_group.py` / `services/fishing.py` / `services/dungeon.py` / `services/gamba.py` / `services/savings.py` / `services/auction.py` / `services/liquidity.py` / `services/mastery.py` ship in the wiring batch. +- **V3 Pillar 5 -- Inbox / persistent notifications**: Market events DM'd you once, raid notifications flew past in busy channels, season-end was announced in `#general` and gone. The inbox is a durable per-user log of every notification-worthy event so a player can run `,inbox` at any time and see what they missed. `services/inbox.py` exposes `post(db, uid, category, title, body, severity, payload, gid)` as the producer surface; every system that wants to notify writes one row through that helper. New commands: `,inbox` (PNG index with severity-coloured rows, NEW pills, posted-time deltas), `,inbox ` (PNG message detail with body wrap + auto-mark-read), `,inbox clear` (mark all read), `,inbox purge` (delete read), `,inbox prefs on|off` (toggle per-category DM mirroring, opt-in to avoid spam). Migration `0240_inbox.sql` ships `user_inbox` with a JSONB payload + a partial index on unread rows, plus `user_inbox_prefs` for the DM toggle. Categories used today: `market_event`, `raid`, `season`, `achievement`, `mastery`, `clan_war`, `governance`, `auction`, `cosmetic`, `system`. New categories don't need a schema change. PNG renderers (`services/inbox_render.py`) compose against `core/framework/render.py`. Hooks from market events / raids / season end / achievements / mastery level-ups / clan war phases / auction outcomes ship in a follow-up commit alongside the rest of the V3 wiring batch. +- **V3 Pillar 2 -- Apex Mastery (cross-system meta-progression)**: Fishing XP, farming XP, dungeon XP, gamba XP, exploit XP, buddy XP, validator XP, crafter XP, trader XP all used to live on their own islands. A player with 10k fishing levels got nothing from it outside fishing. Apex Mastery binds nine disciplines into one career: every minigame emits mastery XP into a per-track row (100-level cap, 1.15^n XP curve), levelling a track grants mastery points (1/level + a milestone bonus every 10 levels), points unlock nodes on a 20-node skill tree with four branches (economy / combat / luck / utility). Effects are permanent passive buffs applied across the bot: `+5% daily`, `+10% savings APR`, `+15% LP yield`, `+10% gamba payout`, `+10% legendary fish rate`, `+10% mining hashrate`, `expeditions 10% faster`, `auction fees cut 25%`, etc. Three commands: `,mastery` (full PNG board with track bars + node tree built on `core/framework/render.py`), `,mastery unlock ` (spend points), `,mastery reset` (paid wipe; cost doubles each reset matching `,buddy respec` / `,delve respec`), `,mastery info ` (inspect a single node). Migration `0237_apex_mastery.sql` ships `user_mastery` (track XP/level), `user_mastery_nodes` (unlocks), `user_mastery_meta` (point budget). `mastery_config.py` is fully declarative -- adding a node is one dict entry. The XP emit hooks land in their respective cogs alongside the per-cog Pillar 10 wiring in a follow-up commit. +- **V3 Pillar 10 -- Continuous Wealth Equalizer (per-tx tax + streaming UBI + Gini-targeting controller)**: The legacy equalizer ran once a day per guild as a single big drain event. Whales extracted value between cycles, UBI was lumpy, and Gini moved in steps instead of a curve. V3 makes the redistribution continuous while keeping the audit trail identical (tax in -> pool -> UBI out, every payer linkable to every recipient, `,wealth flow` / `,drs equalizer cycle` unchanged). Tax is now collected on every economic credit (daily, work, harvest, fishing/dungeon/farming/crafting/gamba claim, swap proceeds, drops, raid winnings, validator rewards, etc.) at a marginal rate that depends on the player's net-worth PERCENTILE in their guild -- which is intrinsically self-balancing because percentile is relative not absolute. Default curve: bottom 50% pays 0%, 50-75% pays 0-2%, 75-90% pays 2-8%, 90-99% pays 8-25%, top 1% pays 30%. The bottom 25% receives a *floor bonus* from the pool on every credit (negative tax, capped per UTC day so it's not farmable) -- the "poors get a little extra" promise. P2P transfers are NOT taxed (would discourage trade); only credits originating from the economy itself. UBI is then streamed back out continuously: every 15s a small fraction of the pool drains to N rank^2-inverse-weighted recipients (poorer = more love). An auto-balanced PI controller (`services/cwe_controller.py`) reads Gini every 5 minutes and adjusts `tax_mult` + `ubi_mult` in `[0.25x, 2.0x]` to hold `Config.CWE_GINI_TARGET = 0.65`. New commands `,wealth tx ` (per-tx simulator with PNG card) and `,wealth controller` (PI loop history with PNG line chart). Migrations `0248_cwe_continuous.sql` (adds `linked_payer_id` + `cycle_id` to the log; new `cwe_controller_log` + `cwe_user_tx_state`) and `0249_cwe_seed_curve.sql` (default curve seed). All renderers built on `core/framework/render.py`. Legacy `run_cycle` is preserved as a no-op settle-and-close for admin scripts. Long-term stability: percentile-driven, self-balancing, smoothed consumption, paired with Pillar 9 (LP exempt) and Pillar 8 (real chart impact) for an economy that auto-corrects against rich-get-richer feedback loops. +- **V3 Pillar 9 -- LP is now permanently tax-exempt + one-shot historical restoration**: The pre-V3 wealth tax counted LP positions in the OWED amount (via `compute_bulk_net_worth.lp_value`) but skipped LP in the DRAIN because mid-cycle LP unwinds blow up. The asymmetry meant a player who put everything into LP to support liquidity got max-bracketed and paid it out of whatever non-LP surface they had left -- effectively a tax on backing the AMMs. New `services.net_worth.compute_bulk_lp_value(gid, db)` returns `{uid: lp_usd}` and the equalizer subtracts it from each user's gross NW before applying brackets and before the per-cycle drain cap. The audit log gains `gross_nw_usd` + `taxable_nw_usd` columns (migration `0247`) so `,drs equalizer` can show the carve-out explicitly. `Config.WEALTH_TAX_LP_EXEMPT` defaults True (toggle off to restore the legacy behavior if an operator ever wants to). Migration `0246` ships the restoration audit tables (`wealth_lp_restoration`, `wealth_lp_restoration_runs`) and `services/lp_restore.py` walks every historical `WEALTH_TAX` row, estimates the LP-attributable fraction via the user's current `lp_value / net_worth` ratio, and refunds USD-equivalent to the wallet (rather than re-minting LP shares which would corrupt pool reserves). Idempotent: a finished-row in `wealth_lp_restoration_runs` gates the pass to once-per-guild. New admin commands `,admin lp_audit` (dry-run preview) and `,admin lp_restore` (commit) plus `Config.GENESIS_LP_RESERVE` for operators who want to top up the refund budget beyond the current pool balance. Long-term stability: LP is the only asset class where holding it *helps everyone* (deeper books, lower slippage, more swap volume); exempting it gives wealth a non-rent-extracting home that aligns player incentives with economy health. +- **V3 Pillar 8 -- swap-impact on the chart is real again**: `,buy` and `,sell` (the market-maker path in `services/trade.py`) used to rebalance the user's position but never touch `crypto_prices.price` or write a candle row, so the chart stayed flat even on large market orders. Players reported "I just dumped a million dollars and the chart didn't move" -- they were right. The AMM path (`services/swap.py`) already had `apply_swap_oracle_nudge` writing both legs into the oracle + candles; V3 extracts the per-symbol push into a shared `_nudge_symbol_oracle` and adds `apply_trade_oracle_impact(db, gid, sym, usd_value, direction)` that the buy/sell paths now call after the atomic block commits. Same `_SWAP_ORACLE_NUDGE_CAP` (2.5%) clamp so a single market order can't yank the chart off-screen; whales who want more impact have to keep trading. Pegged wrappers + stablecoins are skipped (their oracle is clamped by the drift loop each tick). Test surface: `tests/test_swap_oracle_impact.py` pins the curve, the buy/sell symmetry, the stablecoin skip, and the AMM regression so this can't silently break again. + +### New Features +- **`,delve respec` -- refund every spent delve stat point for a USD fee**: Players who built a Power-stacked Warrior then rerolled into Archer (or who just mis-spent a few points early) were stuck with the allocation forever, because stat-point spends were one-way and `,delve reroll` deliberately preserves them. New `,delve respec` (aliases `restat`, `reset_points`) zeroes `hp_alloc / atk_alloc / spd_alloc / int_alloc` back to 0 so the player can reallocate from scratch via `,delve upgrade`. Cost doubles per respec on the same delver (`$10,000 -> $20,000 -> $40,000 -> ...`) mirroring the `,buddy respec` curve. Refused mid-run so a mid-fight panic spend can't be undone, charges wallet+bank atomically, bus-publishes `delve_stat_respec`, and rebuilds `hp_max` from class base so the player's max HP snaps back to baseline (with `current_hp` clamped). New `stat_respecs_used` column on `user_dungeon` plus a non-negative CHECK constraint (migration `0236_dungeon_stat_respec.sql`). + +### Documentation +- **`,delve upgrade` panel now explains how the point system actually works**: The empty-form panel used to show just "Each level grants 1 stat point." plus current allocations. Players still hit `,help` to figure out that points are shared across all four lanes, that allocations survive class rerolls / gear swaps / run resets, and that the per-point payoffs scale specific combat stats (SPD -> crit + first-strike, INT -> spell damage for Mage / Druid, etc). Reworked the panel into three labelled sections: **How points work** (1 point per level, shared pool, sticky across rerolls, only `,delve respec` refunds them -- with the live current respec price quoted), **Current allocations** (per-stat current spend with the resulting combat-stat delta), and **Per-point payoff** (what each point buys, with the secondary effects -- SPD's crit + first-strike, INT's spell-damage lane -- called out inline). The `,delve respec` command and its "doubles each respec" wording is also surfaced as a footer hint so players know reallocation is possible before they commit. `,delve help` gains a dedicated "Class + stats" section listing `,delve class` / `,delve reroll` / `,delve upgrade` / `,delve respec` together so the four related commands are discoverable from one place instead of scattered. + +### Bug Fixes +- **Disco AI chat: way fewer "AI didn't respond" cards on Ollama deployments**: Three compounding issues caused replies to fail roughly 1-in-3 in the screenshot the user shared (`gemma4:31b-cloud` working in some replies but bouncing in others). (1) `complete_ollama` only retried 5xx ONCE with a 0.5s sleep and didn't retry 429 at all; hosted Ollama Cloud routes routinely 429 / 502 / 503 under load. Now retries up to 3 attempts with exponential backoff (0.5s, 2.0s) and honours the `retry-after` header on 429. (2) The outer `asyncio.wait_for` in all three chat handlers (ask_cmd / handle_ai_reply / handle_ai_mention) was 75s text / 120s vision -- a heavy 31B Ollama Cloud model at ~22 tokens/sec can legitimately take 25-30s for an 8k-token reply, and a 3-iteration tool loop trivially adds 30-60s on top. Bumped to 180s text / 240s vision so legitimate slow replies actually land instead of getting culled by the outer timeout. (3) The "AI didn't respond" card was completely opaque -- no clue whether to retry immediately or wait. New `_ai_error_hint` translates the bridge's error reason (`http_429`, `http_502`, `network_TimeoutError`, `empty_response`, `timeout`) into a brief parenthetical: "AI didn't respond (rate limited). Try again in a sec." or "AI didn't respond (model busy). Try again in a sec." `_stream_ai_chat_to_message` now returns the reason as a third tuple element and the empty-AI log line includes it for operator visibility. +- **Disco AI chat: lookup queries (`@Disco look up X`, "search for Y", URLs) get web tools again**: The earlier fast-path optimization returned an EMPTY tool schema list when no `tools.json` game keyword matched, which was meant to speed up casual chat ("hi how are you") -- but it also nuked tools for explicit data requests like "Please look up information about WEBN" because no Discoin game vocab matched. The model then had no `data.web_search` / `data.web_fetch` to call and either bailed or hallucinated. New `_LOOKUP_INTENT_RE` keeps the base tool set in scope when the message contains "look up", "search for", "find info", "tell me about", "price of", "news on", or any URL -- so genuine lookup queries still get tools while casual chat keeps its fast path. +- **Disco AI chat: revert to "spinner-then-chunked" UX (live token streaming felt slower, dropped the token-count footer)**: The previous release real-streamed each OpenRouter SSE delta straight into the Discord placeholder, which sounded faster on paper but in practice was much worse: each delta is typically 2-3 chars and Discord's per-message edit throttle (~0.85s) capped visible updates to ~2-3 chars/sec, so replies looked like they were being typed by hand. The trailing usage chunk on the SSE stream also dropped silently on provider-side hiccups, leaving the reply footer empty. Both the no-schemas fast path and the post-tool-loop final pass now go back to a synchronous `_complete_for_provider` call followed by `_fake_stream_text`, which restores the polished "thinking spinner -> chunked paint-in" rhythm AND the `gemini-2.5-flash | 3 tools | 4.2s | 1,247 tokens` token-count footer (now also wired up for Ollama via a new `_usage_out` parameter on `complete_ollama` -- the OpenAI-compat endpoint already returns the usage block, the bridge just wasn't capturing it). `_FAKE_STREAM_CHUNK_CHARS` and `_FAKE_STREAM_CHUNK_SLEEP` are restored to 24 / 0.04. Provider routing (Ollama vs OpenRouter) is preserved end-to-end so the previous Ollama-routing fix still stands. Dead helpers (`_stream_for_provider`, the include_usage rescue retry) removed. +- **Disco AI chat: actually route to Ollama when TOOLS_BACKEND=ollama**: The streaming bridge always called OpenRouter's `complete_stream` and OpenRouter's `complete` regardless of the resolved provider, so a guild configured with `TOOLS_BACKEND=ollama` + an Ollama model like `gemma4:31b-cloud` sent every chat turn through OpenRouter and got a hard 400 (`"gemma4:31b-cloud is not a valid model ID"`) -- which silently produced no tokens and surfaced "AI didn't respond" to the user. New `complete_stream_ollama` in `core/framework/ai/client.py` mirrors the OpenRouter streaming variant but points at the Ollama OpenAI-compat endpoint, and new `_stream_for_provider` / `_complete_for_provider` helpers in the bridge dispatch every streaming + fallback call to the right backend based on the `(provider, model)` that `_resolve_tools_pick` already returned. Both `complete_with_agent_tools` and `complete_with_agent_tools_stream` now honour the provider choice end-to-end -- no path can accidentally bounce an Ollama-only deployment to OpenRouter. +- **Disco AI chat: rescue `stream_options.include_usage` rejections**: `complete_stream` now accepts an `include_usage` flag (default True) and an `_error_out` list. A few providers behind OpenRouter (notably some Gemini and Llama routes) reject the `stream_options` field with a 400, which used to silently produce zero tokens. The bridge captures the error reason, and on a 4xx with `include_usage=True` on the OpenRouter side it retries the same request with the field stripped before falling through to the non-streaming completion. Empty-response error events now carry the underlying reason (`http_400`, `network_TimeoutError`, etc) instead of a generic `empty_response`, and the bridge logs include provider, model, and error reason so future empty-reply incidents are diagnosable from a single log line. +- **Disco AI chat: more honest user-facing error message**: Replaced the "AI is taking a nap, try again in a bit." cute card across `ask_cmd` / `handle_ai_reply` / `handle_ai_mention` with "AI didn't respond. Try again in a sec." -- the previous wording implied the bot was idle by choice when the real cause was almost always a transient upstream provider failure the user can immediately retry through. +- **Disco AI chat: stop the "AI timed out" / "connection died" failures**: The OpenRouter HTTP client capped concurrent AI calls at a global semaphore of 8, which sat OUTSIDE the per-request `asyncio.wait_for` budget in `cogs/help.py`. A multi-iteration tool loop uses 3-4 sequential slots, so 2-3 active users were enough to queue everyone else behind the semaphore -- new requests never even started their HTTP call before the 40s outer timeout fired, producing the "AI timed out, try again in a sec" message that looked like a connection drop. Raised the cap to 32 and bumped `TCPConnector` to `limit=64, limit_per_host=32, keepalive_timeout=75, enable_cleanup_closed=True` so aiohttp's pool (not a Python lock) is the real flow-control point. + +### New Features +- **Disco AI chat: fast-path streaming for casual chat (no game vocab, no image)**: `_select_tool_schemas` now returns an empty schema list when the message has zero `tools.json` keyword matches AND no image attachment, which signals the streaming bridge to take its direct-`complete_stream` branch and skip the non-streaming tool-call dispatch round entirely. Casual chat (greetings, opinions, jokes -- the dominant case for `,ask`, AI mentions, and threaded replies) now goes straight from prompt build to streamed tokens, cutting 1-3s of dead air per reply. Real game queries still always hit a trigger key, and image attachments still get the vision schemas, so the change only removes overhead from the casual-chat case where tools were never going to fire anyway. +- **Disco AI chat: parallel tool execution within a single round**: The agent tool loop used to run fan-out tool calls serially -- a query that asked for `wallet.portfolio + market.snapshot + data.web_search` in one round paid the sum of all three latencies. New `_execute_one_tool_call` helper wraps the parse-args-and-run path with safe exception handling, and both `complete_with_agent_tools` and `complete_with_agent_tools_stream` now `asyncio.gather` the fan-out. Ordering of the appended tool turns (and of the side-effect yields like `tool_call` / `image_generated` / `search_sources` / `approval_required` in the streaming variant) is preserved so the OpenAI tool_call_id linkage stays intact and the UI still renders progress deterministically. Typical 3-4-tool rounds now resolve in ~max(latency) instead of ~sum(latency), saving 1-3s per round. +- **Disco AI chat: token-count footer preserved through streaming**: Added `stream_options.include_usage=true` to the OpenRouter SSE payload in `complete_stream`, and the function now accepts an optional `_usage_out` list the caller can pre-allocate to capture the usage block. The streaming bridge threads this through both the no-tools fast path and the final-pass-after-tool-loop branch, then folds the usage into `_accumulated_usage` so the streamed reply's compact footer shows `gemini-2.5-flash | 3 tools | 4.2s | 1,247 tokens` exactly like the non-streamed path did. Providers that don't honour the include_usage flag just skip the extra chunk and the footer falls back to its previous shape -- no failure mode. + +### Changes +- **Disco AI chat: Discord channel-history fetch runs in parallel with chat context**: `handle_ai_reply` and `handle_ai_mention` used to await `gather_chat_context` first (a ~400ms DB fanout), THEN do a Discord API `channel.history(limit=8)` round-trip (~200-500ms) -- ~700ms total on the pre-AI critical path. New `_fetch_recent_chat_block` helper lifts the history fetch into an `asyncio.gather` alongside `gather_chat_context`, so the two overlap into the longer of the two. ask_cmd was already history-free so it's unchanged. ChatContext's `extra_blocks` list receives the prefetched block when both fetches resolve. + +### Changes +- **Disco AI chat: longer HTTP read budget, longer outer timeouts**: `_REQUEST_TIMEOUT` widened from `total=45, sock_read=38` to `total=100, sock_read=90`, and `_REQUEST_TIMEOUT_VISION` from `total=90, sock_read=80` to `total=130, sock_read=120`, so slow-to-first-byte tool-call rounds and vision describes no longer trip false-positive timeouts mid-generation. Outer `asyncio.wait_for` in `ask_cmd` / `handle_ai_reply` / `handle_ai_mention` raised from `40s`/`85s` to `75s`/`120s` (text/vision) to match the new per-call ceiling. `complete_stream`, `complete_with_tool_calls`, `complete_with_tool_calls_ollama`, and `complete_ollama` now pick the vision timeout when the convo contains `image_url` blocks instead of silently inheriting the text-only session default. +- **Disco AI chat: real streaming for the final-pass answer after tool calls**: `complete_with_agent_tools_stream` used to wait for the full final-pass reply via `complete()` and then animate it in via `_fake_stream_text` -- so after the user already stared at the spinner through 1-3 tool iterations, they then waited another 2-5s of artificial chunked paint-in before seeing any text. Swapped the final pass to `complete_stream` so tokens land in the placeholder as the model produces them; the non-streaming `complete()` path is retained as the fallback when SSE drops or returns nothing. Also dropped the artificial `_FAKE_STREAM_CHUNK_SLEEP` (0.04s -> 0) and widened chunks (24 -> 48 chars) for the cases that still use fake streaming -- the Discord-side edit throttle already caps edit frequency, so the per-chunk sleep was pure latency. +- **Disco AI chat: spinner no longer collides with delta edits**: `_AI_SPINNER_TICK` raised from `1.0s` to `1.5s` and `_AI_EDIT_THROTTLE` lowered from `1.0s` to `0.85s` so the spinner forced-edit and the first arriving token-delta edit don't fight for the same throttle window. The old `1.0s/1.0s` rhythm meant the very first token arriving 0.1s after a spinner tick would trip Discord's edit rate limit and the delta would be dropped, making streams look frozen for ~1s. + +### Changes +- **Soften Disco's voice: less degen, less tired**: The core `_DEFAULT_ASK_PROMPT` and the `degen` / AI-channel persona hints leaned hard on "crypto-native", "a bit dry", "sarcastic", "lean into the chaos", and "be a gossip / you've seen it all", which read as burned-out and aggressive in casual chat. Reworded to "crypto-fluent", "lightly playful, occasionally dry, with the odd dig", "match the energy without going feral", and "friendly gossip / you've seen plenty" so Disco still has a personality but doesn't sound like a 4am degen who wants out of crypto. + +### Bug Fixes +- **Wealth tax now actually sees gamba stakes**: `compute_bulk_net_worth` (the input to the daily tax) was reading every wealth surface in the guild *except* `gamba_stakes`, which meant a whale parked entirely in staked GAMBIT/CROWN/VEIN/PIP/EDGE/ACE/NOIR/CHERRY (and accrued pending GBC) had a true NW in the millions but a tax-eligible NW of $0. The bulk path now pulls staked amounts at oracle plus pending GBC at GBC oracle, matching what the single-user `compute_net_worth` already counted. The drain (`_drain_user`) gained a step 10b that liquidates gamba stakes the same way Moon/Lunar stakes are liquidated -- biggest USD value first, decrementing `gamba_stakes.amount` (or `pending_gbc`) directly so the deduction stays inside the cycle's transaction context. + +### Bug Fixes +- **`,wealth equalizer` UBI was paying $0 to 0 people every cycle: `last_activity` never written**: Root cause of why Gini wasn't moving despite the tax phase actually pulling money out of whales: the UBI eligibility filter is `WHERE last_activity > now() - make_interval(days => 7)`, but `last_activity` has no `DEFAULT` in the users schema and **nothing in the codebase ever wrote to it**. So every user's `last_activity` was NULL, and `NULL > anything` is NULL (i.e. false). The eligible set was empty on every cycle since the feature shipped, so the pool grew unchecked and zero redistribution happened. Fixed in two places: (1) `ensure_user` now stamps `last_activity = now()` on every call -- which means every command bumps the user's last-seen via `ensure_registered` middleware. (2) `_eligible_ubi_recipients` and `get_supply_per_active` now treat `last_activity IS NULL` as active so existing players (whose timestamp was never written) get backfilled into the eligible set on the very next cycle instead of having to run a command first. Expected effect: UBI will actually pay out on the next cycle, pool drains, Gini starts moving. +- **NW + drain now cover all 13 stones (not just the 5 from CLAUDE.md spec)**: Migration 0146 added eight themed/meta-economy stones (tide / heart / crypt / blood / bloom / gavel / anvil / chimera) and migration 0228 added gambastones, but `services/net_worth.py` and `services/wealth_equalizer.py::_drain_user` were only counting the original four (hash / lock / vault / liq). So whales hiding wealth in any of the nine other stones were invisible to both the NW calculation AND the tax drain. Fixed: NW (both single-user + bulk paths) and the drain (`_STONE_TABLES`) now iterate all 13 stone tables. The drain step also now prices each stone by its `lp_currency` oracle (was treating every stone's `staked_amount` as $1-pegged -- 0.5 BTC in a hashstone was being drained as $0.50). Themed stones stake in REEL / BUD / RUNE / HRV / BBT / FORGE, and those positions are now correctly NW'd + drainable. +- **`,balance` Items tab + Net Worth: stones priced at oracle (not $1-pegged), all stones shown**: Two compounding display bugs around stones. (1) Migration 0165 narrowed hashstone's accepted_currencies to `(BTC, SUN)` and lockstone's to `(DSC, ETH)`, so the `staked_amount` column for those stones now holds raw BTC/SUN/DSC/ETH amounts. `services/net_worth.py` (both `compute_net_worth` and `compute_bulk_net_worth`) was still summing `to_human(staked_amount)` as if it were $1-pegged stablecoin -- so a hashstone with 0.5 BTC staked was being valued at $0.50 instead of ~$50,000. Fixed both NW paths to read each stone's `lp_currency` and price at the live oracle (stablecoins + USD still treated as $1:$1, everything else priced at `prices[lp_currency]`). (2) The `,balance` Items tab hardcoded a list of four stones (hash/lock/vault/liq), silently dropping `gambastone` AND every themed stone (`tide / heart / crypt / blood / bloom / gavel / anvil / chimera`) -- players who owned any of them saw nothing on the Items tab. Replaced with the same `_STONE_CFGS` iteration `,inventory` uses. (3) The summary tab's `items_value` was being re-derived inline using the same $1-peg assumption; replaced with `nw.items_value` (CLAUDE.md: NW is computed in one place). Net effect: a player with stones staked in non-stable currencies now sees the correct USD value on `,balance`, `,inventory`, the Items summary line, and the overall Net Worth total. +- **Wealth tax: gamba_stakes was actually not being seen (column rename)**: My earlier "gamba is now in NW and drain" fix wrote queries against `pending_gbc`, but migration 0234 had renamed that column to `pending_yield_raw` and added a `yield_target` column (default `'GBC'`, opt-in `'BUD'`). Every call site (`compute_bulk_net_worth`, `_drain_user` step 10b, `,drs gamba`) was wrapped in `try / except: pass` that silently swallowed the resulting `UndefinedColumn` error -- so the bulk NW saw zero pending yield, the drain skipped the gamba block entirely on production DBs, and the DRS view showed `pending_gbc: 0` for everyone. Fixed all three to use `pending_yield_raw` and price each row's pending amount against its `yield_target` oracle (BUD-target positions now correctly priced at BUD instead of mistakenly at GBC). `,drs gamba` also surfaces the yield target per position now (e.g. `PIP -> BUD`). +- **Fix CI: ,drs eq subgroup had two subcommands aliased to "history"**: `drs_eq_cycles` declared `["history", "list"]` and `drs_eq_user` declared `["player", "history"]`. Both live inside the same `drs_eq` subgroup namespace, so discord.py raised `CommandRegistrationError: The alias history is already an existing command or alias` at cog load time and the pytest collection step exploded for every test that ends up importing the cog. Renamed the user one to `userhistory`; `,drs eq history` (alias of `cycles`) keeps the cycle-history read. All 1172 tests now pass locally. + +### Discord Bot +- **DRS round 5: user prefs, active locks, token-wide audit**: Three more `,drs` subcommands round out the audit surface. `,drs prefs @user` lists DM opt-ins on/off across all `user_prefs` flags (mining/transfer/validator/staking/2fa/events/nft/predictions/ape/itemlevelup/whale), PvP flag, and muted-network lists per category. `,drs locks @user` (alias `cooldowns`/`active-locks`) shows every active lock on a player: PvP toggle 1h cooldown (DB-clocked against `pvp_last_exploit` so it matches what the exploit cog enforces), validator `stake_locked_until` per network, outgoing delegation `locked_until` per network/token, Safety Module cooldown per symbol, and outstanding loan obligation. `,drs token ` (alias `coin`/`sym`) is the first GUILD-WIDE audit in DRS instead of per-player: shows price, total counted circulating supply, holder count, supply broken out by bucket (CeFi / DeFi / NPC stakes / delegations / gamba), top-1 and top-10 concentration percentages, and a top-10 holder leaderboard with mentions and percent-of-supply. Includes a bar chart of value-by-bucket priced at oracle. Three new entries added to the Account & Activity help category. + +- **DRS surfaces round 4: daily streak, consumable inventory, cross-network wallets, PvP exploit, mining-group membership**: Five more `,drs` subcommands cover the account-flavour audit angles. `,drs daily @user` reports the player's current streak, last_daily timestamp, and computes 24h eligibility against the DB clock so the value matches what the live `,daily` cog would see. `,drs items @user` enumerates consumable inventory (validator guards, yield guards, gambling saves) priced via `Config.SHOP_ITEMS[*].cost_stable`. `,drs wallets @user` lists every DeFi `wallet_addresses` row grouped by network with all holdings on each, priced at oracle and totalled per network, plus a bar chart of value-by-network. `,drs exploit @user` (alias `pvp`/`heist`) surfaces `exploit_stats` with win rate, defence rate, total stolen vs total lost, and a USD-flow chart -- skips display when both the stats table is empty and PvP is disabled. `,drs guild @user` reads `mining_group_members` to show the player's mining group with founder role, member roster (up to 15), and join timestamps with `[FOUNDER]` and `[TARGET]` tags. New "Account & Activity" help category groups all five. + +- **DRS surfaces round 3: lunar/moon, safety, disc.fun, NFTs, delve, buddy, crafting, savings, trades, work, full net worth**: Twelve more `,drs` subcommands close out the wealth-surface coverage so every category that contributes to `compute_net_worth` has a dedicated audit view. `,drs lunar` shows Tier-1 lunar_stakes (group tokens minting MOON) with session/lifetime earnings, plus the Tier-2 moon_stakes pool (MOON staked for DSD yield). `,drs safety` enumerates AAVE + DSY positions with auto-compound + cooldown flags. `,drs discfun` lists active proto-token holdings (with cost basis) and staked discfun_stakes with pending DFUN. `,drs nft` groups owned NFTs by `(collection, rarity)` and prices each group via the collection's avg-sale-price-by-rarity map, falling back to `mint_price * mint_token oracle` when no sales exist. `,drs delve` reports COPPER/SILVER/GOLD ore stakes, pending RUNE accrual, dungeon level + XP + deepest floor + total kills, and owned party buddies. `,drs buddy` covers FREN staked + pending BUD + battle/storage/egg/nest slot purchases at their config-defined USD prices. `,drs crafting` shows INGOT stake + pending FORGE + crafted inventory priced via `crafting_config.craft_meta` FGD costs. `,drs savings` lists every savings deposit across all symbols, USD-priced. `,drs trades` pulls `user_profiles` lifetime aggregates plus the latest BUY/SELL/SWAP/ARB transactions with volume-by-tx-type chart. `,drs work` surfaces the active job, work-session count, and aggregates the last 50 WORK transactions by payout symbol. `,drs networth` is the catch-all: pulls `compute_net_worth(uid, gid)` and renders all 26 contributing categories sorted desc with USD value + percent of total, plus a horizontal bar chart of the breakdown. Every command DMs the result with channel fallback and logs a `DRS_` action. The Wealth Surfaces help category now spans 3 pages to fit all 21 subcommands. + +- **DRS full-surface x-ray: stakes, validators, mining, LP, stones, gamba, games, loans, timeline**: Nine new `,drs` subcommands give auditors visibility into every wealth-bearing surface on the bot, each rendered with a Pillow chart attachment where it makes sense. `,drs stakes @user` lists every NPC yield-farm position priced at oracle with a horizontal bar chart of value-by-validator. `,drs validator @user` shows own PoS stake across every network the player runs on, plus incoming delegations (with each delegator mentioned) and outgoing delegations (with the validator mentioned), totals priced + a stacked exposure chart. `,drs mining @user` lists rigs, total hashrate, and solo/pool/group mode. `,drs lp @user` prices LP positions per pool by walking current reserves and the player's `lp_shares / total_lp` proportion, with vault-locked + group-pool tagged. `,drs stones @user` reports all five stones (hash/lock/vault/gamba/liq per the CLAUDE.md spec), with level + XP + staked + acquired-at for each. `,drs gamba @user` enumerates every staked game token (GAMBIT/CROWN/VEIN/PIP/EDGE/ACE/NOIR/CHERRY) with pending GBC, lifetime claimed, lifetime auto-compounded, and an auto-compound flag. `,drs games @user [limit]` aggregates `game_sessions` per game_type (with status counts) and pulls GAMBLE-prefixed transactions for a per-game win/loss USD chart. `,drs loan @user` surfaces both standard and SUN loans with health ratio, paid-down amount, principal, collateral, and timestamps. `,drs timeline @user [days]` (alias `activity`/`wealthline`) is the big one: a paginated chronological feed of every transaction in the window with type-emoji prefixes, signed USD delta per event, and a cumulative wealth-flow line chart that fills under the zero axis so drawdowns and recoveries are visually obvious. Every command logs a `DRS_*` action to the unified staff audit feed and DMs the result to the operator with a fallback to in-channel reply if DMs are closed. Two new chart renderers added to `services/equalizer_charts.py`: `render_value_bars` (generic horizontal bar chart, reused across stakes/LP/stones/gamba/validator) and `render_winloss_bars` + `render_timeline`. Added a "💰 Wealth Surfaces" category and a Timeline entry to the existing `,drs` help dropdown. + +- **`,drs equalizer` x-ray dashboard for the wealth-tax + UBI loop**: New DRS Terminal subgroup that surfaces every redistribution event since genesis to trusted auditors. `,drs equalizer` (aliases `eq`, `redistribution`, `taxubi`) shows the pool, lifetime totals, top payers/recipients, and Gini-now plus 24h/7d delta arrows. `,drs eq cycles [page]` paginates every cycle ever run, newest first. `,drs eq cycle <#>` drills into one cycle with the full list of payers + recipients (chunked into 15-row pages so the embed never trips the 1024-char field limit). `,drs eq user @target` paginates that player's entire tax + UBI history and DMs a cumulative-flow PNG chart. `,drs eq chart [gini|pool]` renders a server-side Pillow chart of the Gini coefficient over the last 30 days (with healthy/severe/extreme threshold lines) or a twin-bar tax-in vs UBI-out chart of the last 30 cycles. `,drs eq export` dumps the full `wealth_redistribution_log` as a CSV with `cycle_at_iso, user_id, kind, amount_usd, net_worth_usd` columns and DMs it to the operator. Every command logs a `DRS_EQ_*` action to the unified staff audit feed so operators can see who looked at what. All gated behind the existing `drs_commands` beta feature -- nothing new to grant. + +### Services +- **Wealth Equalizer: rank-progressive tax + pay-everyone UBI + close hideaways to crush Gini**: Players were reporting "I got taxed but never received any UBI". Two bugs were stacking. (1) Tax used a flat marginal bracket on absolute net worth, so anyone above $50k was taxed regardless of where they ranked in the guild. (2) UBI iterated eligible players poorest first with linear inverse-rank weights and a hard `break` once the natural share dropped below `MIN_PAYOUT` ($50). On a typical 30-eligible cycle the share fell below the floor by index ~7, so the bottom of the leaderboard got paid but everyone else in the eligible set got $0 even after paying tax. Reworked both halves: tax now sorts holders richest first, stacks a rank multiplier (`2.0x` at #1, decaying linearly to `0.0` at the 50th percentile) on top of much steeper brackets (2%/5%/10%/20%/50% with the exempt floor lowered to $10k), and caps drain at 40% of net worth per cycle. The bottom half of the leaderboard now pays $0. UBI guarantees a $1 floor stipend to every eligible recipient when the pool can cover it, then distributes the remainder by rank^2 inverse weights up to $50k -- the poorest gets ~4x the bonus of the median and nobody who paid tax in phase 1 walks away with $0 in phase 2. `WEALTH_UBI_TOP_EXCLUSION_COUNT` dropped from 3 to 1, payout fraction bumped from 0.80 to 0.95. Also closed every easy wealth-hiding loophole. Net worth now includes gambastones (matching the CLAUDE.md spec of `hash + lock + vault + gamba + liq`) and non-USD savings deposits priced via oracle -- previously a whale could shelter wealth in either and it was invisible to the tax. The drain (`_drain_user`) now liquidates non-USD savings, gambastones, PoS validator own stake across every network, PoS delegations, lunar mint stakes, and Moon Pool stakes -- in addition to the existing wallet/bank/USD-savings/CeFi/DeFi/stones/rigs ladder. LP positions and NFTs are still skipped (they need bespoke unwind paths) but their value is still counted in the *owed* amount so a whale who hides everything there gets max-drained against whatever liquidatable surface remains. Expected effect: Gini falls hard cycle over cycle from the current 0.991. +- **Give Disco AI the real-world date so it stops hallucinating "today"**: The chat system prompt's TIME: block only carried weekday + HH:MM (e.g.; "Sun 14:30 UTC"). When players asked "what's the date?" or "what year is; it?" the model had nothing to anchor on and answered from training data, (`c9ccf55d`) + +--- + +## [main] — 2026-05-10 + +### Discord Bot +- **Disco AI now knows the real-world date and time**: The chat system prompt's `TIME:` block only carried weekday and `HH:MM` (e.g. `Sun 14:30 UTC`), so when players asked "what's the date?" or "what year is it?" the model answered from training data and was months stale. Replaced with a `CURRENT DATE AND TIME` source-of-truth line that includes the full weekday, month, day, year, ISO date and vibe slot, and tells the model to quote from that line instead of guessing. +- **Wealth Equalizer: drain across asset classes + widen UBI to top 3**: Live cycle output showed the equalizer collected $79.9M into the pool; across two cycles but paid $0 UBI to 0 recipients, and whales with; $10B+ NW were only contributing $1M because the drain was still (`6226cb6d`) +- **Fix wealth equalizer: tax full net worth + restore UBI small-pool fallback**: Two compounding bugs meant the daily wealth-equalizer cycle ticked up; ``cycles`` but produced zero ``tax`` and zero ``ubi`` log rows:; 1. The bracket ladder was being applied against *liquid stablecoin (`d5342052`) + +### Services +- **Fix gamba cashout erasing GBC when USD credit rounds to zero**: cashout_gbc burned the player's GBC via update_wallet_holding(-amount); unconditionally, then only credited USD inside an 'if usd_credit_raw > 0:'; guard. When usd_credit_human rounded to 0 raw (dust amount, or after the (`d47a4780`) + +### Configuration +- **Cut gamba stake yields + win mints by 75% to slow emission**: GAMBA_STAKE_GBC_PER_DAY, GAMBA_STAKE_BUD_PER_DAY: 0.01 -> 0.0025.; 1000 staked PIP / ACE / VEIN / EDGE / NOIR / CHERRY / GAMBIT / CROWN; now drips 2.5 GBC (or BUD) per day instead of 10. (`9c870e69`) + +--- + +## [main] — 2026-05-09 + +### Bug Fixes +- **`,gamba cashout` no longer erases GBC without crediting USD**: `services/gamba.py::cashout_gbc` burned the player's GBC unconditionally via `db.update_wallet_holding(-amount)` and then only credited USD inside an `if usd_credit_raw > 0:` guard. When the credit rounded to zero raw (dust amount, or a GBC oracle that had drifted toward the `1e-9` floor), the burn still committed but the credit was silently skipped -- the receipt rendered `Credited: $0.00 to your wallet.` and the GBC was gone for good. The burn + credit also weren't transactional, so a transient DB error on the credit call could leave the same desync. Fixed by (1) computing the USD credit upfront and raising a clear `ValueError("Amount too small to cash out -- USD credit would round to zero. Try a larger amount.")` BEFORE touching the wallet, and (2) wrapping the burn + credit in `async with db.atomic():` so they commit or roll back together. + +### Discord Bot +- **Drop channel.typing() from Disco AI hot paths to escape /typing rate limit**: Smoking gun in the production Railway logs:; POST .../channels/1467875516718514226/typing responded with 429.; Retrying in 3.00 seconds. (×12 in three minutes) (`3971ab83`) +- **Surface Disco AI silent-bail gates as a reaction so we can diagnose**: handle_ai_mention and handle_ai_reply have four "return silently" branches; (premium gate, per-user cooldown, ai_chat_enabled=False, missing; OPENROUTER_API_KEY). When one fires, the user sees absolutely nothing -- (`c32f004c`) +- **Fix ,ai status crash and stop echoing Discord 429/40062 to users**: Two surfacing bugs from rapid back-to-back testing of ,ai toggle / ,ai; status / ,ask:; 1. ,ai status crashed with NameError: name '_P' is not defined. The (`28bed952`) +- **Fix ,ai status display + ,ai toggle direction (column vs feature key)**: cogs/ai.py was looking up flags on the get_ai_flags() return dict using; the underlying column name (ai_chat_enabled, ai_mm_enabled, ...), but; that dict is keyed by the short feature name (chat, mm, commentary, (`f877e986`) +- **Make Gamba Network a first-class wallet network so GBC + game tokens land in ,wallet**: The Gamba Network shipped with Config.GAMBA_NETWORK_SHORT="gam" and nine; tokens (GBC + 8 game tokens) but nothing was registered in; constants/validators.py::NET_SHORT, so ,wallet create gam failed and the (`fa8cd7c1`) +- **Flip chess/checkers boards with side to move + readable coordinate gutter**: The PNG boards previously flipped based on viewer identity, but the same; public match message is shown to both players, so whoever moved last saw; their pieces upside-down on the opponent's turn. flip is now derived from (`7182e784`) +- **Per-position yield target on gamba game-token stakes (GBC default, BUD opt-in)**: Implements the spec: each gamba_stakes row picks one yield direction --; GBC (existing default) or BUD. A staked PIP / ACE / VEIN / etc. drips; one or the other, never both. Players who want a split open separate (`f6ae57d9`) +- **Close the circular buddy <-> gamba loop: BUD <-> all 8 game tokens**: Building on the BUD <-> GBC pair shipped last commit, register the; eight Gamba game tokens (GAMBIT / CROWN / VEIN / PIP / EDGE / ACE /; NOIR / CHERRY) as bidirectional ,buddy convert partners. This closes (`26d4768b`) +- **Add BUD <-> GBC bidirectional convert + hotfix gamba cashout alias**: The GBC cashout commit introduced two issues:; 1. Alias conflict crashed the bot. ,gamba cashout was registered with; aliases [burn, sellgbc, withdraw] but ,gamba unstake already owned (`1a09c0db`) +- **Add GBC -> USD burn cashout to the Gamba Network**: GBC was a closed-loop network coin -- earn it via stake yield on the; eight game tokens (PIP / ACE / VEIN / EDGE / NOIR / CHERRY / GAMBIT /; CROWN), spend it at the Gamba Shop. There was no USD off-ramp, so a (`af2f2741`) +- **Drop Mines PNG, make play locks per-game so games run concurrently**: Two fixes after the player-facing test on mobile:; 1. Mines was double-rendering. The Pillow tile grid was rendered ABOVE; the embed via attachment:// AND Discord drew the actual interactive (`1da8f20a`) +- **Render Mines, Blackjack and Roulette as Pillow PNG attachments**: Extends the chess.com-style PNG pipeline from chess + checkers to the; three player-facing games that still rendered as plain embed fields.; Same architecture: services/board_render.py adds a renderer per game, (`66f0232a`) +- **Render chess + checkers boards as chess.com-style PNG attachments**: Both prior rendering paths had real problems on real clients. PR #770's; ANSI colour codes got stripped on iOS / mobile Discord, leaving the; checkers board as identical dots on a uniform background -- unreadable. (`e3d0c329`) +- **Surface VEIN mint on win embed; add chess/checkers AI difficulty**: cogs/play.py mines was calling bare set_tx() on its result embed instead; of self._set_tx(), which is the only call site that inlines the; ctx._gamba_notes stash from _apply_gamba_hooks. The VEIN mint already (`043cdc69`) +- **Redesign chess + checkers boards: unified ANSI, professional look**: Both game embeds were on visibly different rendering systems and; players reported they "looked like a side project". Chess used a; plain monospace block with one bland background; checkers stacked (`d9bb37de`) +- **Fix ,group lp deposit/withdraw/status crashing with "No pool found for COOK/USD"**: services/group_lp.py was hard-coded to look up the founder's vault; treasury action against a TOKEN/USD pool, but group tokens never get; a USD pool by design -- seed_group_genesis_pools mints TOKEN/mBTC, (`2922681d`) +- **Gameplay UI/UX overhaul: remove dead code, polish core conventions**: Production-prep cleanup pass touching every layer of the bot:; cogs/hub.py: 638 -> 41 lines. The standalone Hub view + embed builder; were already retired (the cog had a one-line shim that delegated every (`00cf1af6`) +- **Add interactive ,botinfo dashboard with runtime sparkline charts**: The ,help info page (and the new ,botinfo / ,about / ,uptime / ,version; top-level command) now opens a multi-section view -- Overview, Runtime,; Charts, Network, Services, Commands -- driven by a Select dropdown with (`f03719ed`) +- **Track every command invocation + ,admin commandstats DM dump**: Server admins had no way to see how active each game / feature is.; Add per-invocation usage tracking and an admin DM dump:; - migration 0232_command_usage.sql: command_usage detail rows (`c42eeb05`) +- **Fix CeFi trade burn supply + admin double-count for circulating supply**: cogs/trade.py:; - Buy/sell calls update_circulating_supply for the burn portion, but that; helper only touches guild_tokens. Built-in tokens (BTC, ETH, USDC, AAVE, (`dc40bcac`) + +### Framework +- **Fix Disco AI not responding to @mentions and ,ask reply threads**: Discoin.on_message was calling internal_commands.maybe_handle BEFORE; the AI mention / reply handlers, and maybe_handle was wired to treat; plain @bot pings as a command-invocation surface. For admin or (`62348db6`) +- **Rename StakePanelView._refresh -> _redraw to stop shadowing discord.py**: core/framework/staking.py defined StakePanelView._refresh(self) as the; panel's "re-render embed with latest state" helper. But; discord.ui.View._refresh(self, components) is a discord.py-internal (`9d133fd8`) +- **Revert chess + checkers boards to emoji rendering for mobile parity**: PR #770's ANSI redesign rendered as broken on iOS / mobile Discord.; The ansi code-block backgrounds and foreground colors get stripped on; mobile clients, so checkers red and black pieces collapsed to identical (`47bcb69b`) + +### Changes +- **Regenerate uv.lock so Pillow is installed in the Docker build**: The Dockerfile runs `uv sync --frozen --no-group test --no-group docs`; which refuses to deviate from uv.lock. The previous commit added; pillow to pyproject.toml + requirements.txt but didn't regenerate the (`9ee34c4c`) + +--- + +## [main] -- 2026-05-09 + +### Bug Fixes +- **Wealth Equalizer now actually drains rigs / stones / crypto, and UBI eligibility widens**: Two follow-up fixes after seeing the live cycle output -- $79.9M sitting in the redistribution pool while UBI paid $0 to $0 recipients across both cycles, and whales like meowcow007 (NW $10.5B) only paying $1.1M because the drain was wallet / bank / USD savings only. (1) `_drain_user` in `services/wealth_equalizer.py` now walks every liquidatable asset class in priority order until owed is satisfied: wallet -> bank -> USD savings -> CeFi crypto holdings (sold at oracle, biggest USD value first, `update_holding` decrements circulating supply on the burn) -> DeFi wallet holdings across all networks (same path) -> stone staked_amount on `hashstones / lockstones / vaultstones / liqstones` (biggest stake first, deducted via `GREATEST(0, staked_amount - $)`) -> mining rigs (sold back at 50% book value, biggest by total book first). LP positions and NFTs are still counted in the owed amount via `compute_bulk_net_worth` but skipped by the drain because mid-cycle LP unwinds have too many price-impact / insufficient-reserve failure modes. The per-cycle cap moves from "25% of liquid" to **25% of net worth** so a $1B-NW holder pays at most $250M per cycle even though their bracket rate would otherwise top out at 10% of NW; a liquid-only cap was the loophole that let illiquid whales coast forever. (2) `WEALTH_UBI_TOP_EXCLUSION_COUNT` drops from 10 to 3 so the recipient set isn't empty in small / mid-sized guilds -- the top three holders still get nothing, but everyone else active in the last `WEALTH_UBI_ACTIVE_DAYS` is in. Combined with the earlier small-pool fallback, this means the $79.9M already in the pool will start paying out on the next cycle. `,wealth` description and `docs/admin-guide/economy.md` updated. + +### Changes +- **Gamba network emission cut by 75% across stake drips and win mints**: The supply curve was running too hot. `GAMBA_STAKE_GBC_PER_DAY` and `GAMBA_STAKE_BUD_PER_DAY` drop from 0.01 to 0.0025 (1000 staked PIP / ACE / VEIN / etc. now drips 2.5 GBC or BUD per day instead of 10), and `GAMBA_TOKEN_MINT_PER_USD_WIN` drops from 0.50 to 0.125 (winning $20 in mines now mints 2.5 VEIN instead of 10). Both ends of the loop slow together so the relative balance of "mint a token, stake it for drip" is unchanged -- the absolute throughput is just lower, and the closed-loop GBC -> USD burn cashout + the daily wealth-equalizer game-token burn phase keep more headroom against circulating supply. + +### Bug Fixes +- **Wealth Equalizer was running but never taxing or paying UBI**: Two compounding issues meant the daily cycle ticked up `cycles` but produced zero `tax` and zero `ubi` rows. (1) The bracket ladder was being applied to *liquid stablecoin holdings* (wallet + bank + USD savings) with a `$50k` exemption floor -- but most player wealth lives in stones, staking, LPs, and NFTs, so almost nobody crosses the floor on liquid alone and the redistribution pool never accumulated anything to pay out. (2) PR #788's rank-weighted UBI removed the pre-existing fallback for small pools: when the poorest player's natural share `spendable * 2/(n+1)` is already below `WEALTH_UBI_MIN_PAYOUT`, the loop hit `if share < min_pay: break` on the very first recipient and paid nobody, even after the pool had grown enough to fund flat `min_pay` to the bottom of the leaderboard. Fixed both: the bracket ladder now applies against each player's full net worth (read from `services.net_worth.compute_bulk_net_worth`) so a whale hiding wealth in stones is on the ladder, while the drain still pulls from liquid only and is still capped at `WEALTH_TAX_MAX_DRAIN_PCT = 25%` of liquid per cycle. UBI now detects the small-pool case (poorest's natural share < `min_pay`) and falls back to flat `WEALTH_UBI_MIN_PAYOUT` to the poorest k recipients the pool can afford, instead of bailing out of the whole loop. `,wealth` description and admin docs (`docs/admin-guide/economy.md`) updated to reflect the new "tax on NW, drain from liquid" model. + +### Changes +- **Wealth Equalizer UBI now pays every active player outside the top of the leaderboard, weighted by rank**: The old eligibility gate was a hard `WEALTH_UBI_NET_WORTH_CEILING = $25k` floor with a flat per-head split, so the pool only reached the very poorest few and everyone in the upper-middle of the leaderboard saw nothing on cycle day even though the tax came directly out of holdings they were trying to compete with. The cap also meant that on a server where the median net worth had drifted past $25k, UBI silently paid zero recipients. Replaced the ceiling with `WEALTH_UBI_TOP_EXCLUSION_COUNT = 10`: the top ten holders by net worth on the guild leaderboard are excluded, and every other active player (within `WEALTH_UBI_ACTIVE_DAYS`) is in. Distribution is now a linear inverse-rank weight -- poorest eligible gets weight `n`, richest eligible gets weight `1`, sum `n*(n+1)/2` -- so a poorer player's stipend is a proportionally larger slice of the spendable pool than someone two ranks below the cutoff. Per-recipient `[WEALTH_UBI_MIN_PAYOUT, WEALTH_UBI_MAX_PAYOUT]` clamps still apply (sub-min shares are skipped, super-max shares are clamped and the leftover stays in the pool for the next cycle), and the 80% `WEALTH_UBI_PAYOUT_FRACTION` carry-over is unchanged. `,wealth` shows the new "outside top 10 -- rank-weighted" description; `,wealth flow` / `,wealth top` / `,wealth me` are unaffected since they read the same `wealth_redistribution_log` rows. Admin docs (`docs/admin-guide/economy.md`) updated. + +### Bug Fixes +- **Disco AI hot paths no longer get stuck on Discord's per-channel `/typing` rate-limit bucket**: `ask_cmd`, `handle_ai_mention`, and `handle_ai_reply` all wrapped `gather_chat_context` in `async with channel.typing():`. The typing context manager has to await the first `POST /channels//typing` HTTP send before yielding control to the inner `await`, so when that channel's typing bucket saturates (5 req / 5 s) the bot silently stalls for 15+ seconds before the placeholder ever lands. Players hitting the bot more than once every few seconds saw "no response at all" because every retry compounded the queue, and Railway logs filled with `POST /channels//typing responded with 429. Retrying in 3.00 seconds.` (12+ in three minutes). Dropped the typing context from all three handlers -- the `_thinking..._` placeholder + streaming spinner already give the user "bot is working" feedback without the parallel HTTP cost. The ambient handler keeps its short typing wrap on purpose (it's gated behind 8% probability + 3-min channel cooldown so it doesn't contribute to the saturation problem). + +## [main] -- 2026-05-08 + +### Changes +- **Disco AI mention / reply silent-bail surfaces a reaction so we can tell which gate fired**: The four "fail silent" branches in `handle_ai_mention` and `handle_ai_reply` (premium gate, per-user cooldown, `ai_chat_enabled=False`, missing `OPENROUTER_API_KEY`) used to return without any user-visible signal at all -- which is exactly what was happening in the field, with players reporting "Disco isn't responding" when a 5s cooldown was active or the chat flag had been toggled off. Each silent-bail now adds a single emoji reaction on the user's message via the new `_react_silent_bail` helper: 🔒 premium, 🐌 cooldown, 🔇 chat disabled, 🔑 API key missing. Reactions ride a separate Discord rate-limit bucket from message sends, so they keep working even when the chat HTTP bucket is saturated (40062). The helper also logs a structured `[ai] silent bail uid=… gid=… reason=…` line so a follow-up support reply can pin down the gate without re-deploying. + +### Bug Fixes +- **`,ai status` no longer crashes with `name '_P' is not defined`**: The status footer fell back to a module-level `_P` constant that was never declared in `cogs/ai.py` (the rest of the cog uses `ctx.prefix or "."` directly), so every invocation aborted before rendering the embed. Switched the lone `_P` reference to the same `ctx.prefix or "."` fallback used by every other surface in the file. The display flag fix from earlier in this same release was actually working in the DB; nobody could see it because the status command was bombing on the way out. +- **Stop echoing Discord's "429 Too Many Requests (40062)" rate-limit errors back to the user as a red error embed**: When a command's `ctx.reply` got service-rate-limited, the resulting `discord.HTTPException` propagated all the way to `Discoin.on_command_error`, which rendered the raw error string via `ctx.reply_error(str(error))`. Players saw a confusing "429 Too Many Requests (error code: 40062): Service resource is being rate limited." card, AND the error reply itself was another HTTP send that compounded the rate limit. Added a 429/40062 short-circuit at the top of the unexpected-error branch: log + record as warning, but no user-facing reply. Discord 40062 is a transient bucket-level throttle, not a command bug. +- **`,ai status` now reflects reality and `,ai toggle` actually toggles**: Both commands looked up flags on the `get_ai_flags()` dict using the underlying column name (`ai_chat_enabled`, `ai_mm_enabled`, ...), but the dict is keyed by the short feature name (`chat`, `mm`, ...). Result: `,ai status` always showed every flag as OFF regardless of the real DB state, and `,ai toggle ` always read `current=False` and wrote `True` -- so the "toggle" was a one-way set-to-on that masked any attempt to disable a feature, and players reasonably reported that flipping `chat` did nothing visible. Switched both call sites to look up by feature key. Status now mirrors the DB; toggle flips correctly between ON and OFF. +- **Disco AI now responds to `@mentions` and `,ask` reply threads again for admins / beta users**: `Discoin.on_message` was calling `internal_commands.maybe_handle()` BEFORE the AI mention / reply handlers, and `maybe_handle` was wired to recognise plain `@bot` pings as a command-invocation surface. For admin or beta-flagged users that meant any first word matching a registered internal command (`help`, `balance`, `buy`, `ask`, `send`, ...) silently routed the message back to that prefix command instead of reaching `handle_ai_mention` / `handle_ai_reply`. Discord's reply-feature auto-ping made replies hit the same trap, so the second turn of any `,ask` thread evaporated. Reordered `core/framework/bot.py` so reply-to-bot and `@mention` checks run first, and tightened `core/framework/internal_commands.maybe_handle` to only match the explicit `bot ` / `disco ` / `discoin ` / `assistant ` text invokers -- never bare mentions. Net effect: `@Disco how do I X?` and follow-up replies to `,ask` results now always reach the AI pipeline regardless of permissions. +- **Gamba Network is now a first-class wallet network -- GBC + game tokens land in `,wallet`**: The Gamba Network surface was rolled out with `Config.GAMBA_NETWORK_SHORT = "gam"` and nine tokens (GBC + GAMBIT / CROWN / VEIN / PIP / EDGE / ACE / NOIR / CHERRY) but the network itself was never added to `constants/validators.py::NET_SHORT`, so `,wallet create gam` failed with "Unknown network gam", `,wallet list` could never surface a Gamba balance, and the entire token surface was stored in `crypto_holdings` (CeFi) -- the only earn-only network coin not living in the user's DeFi wallet alongside REEL / RUNE / BUD / HRV / INGOT. Players reasonably reported "there is no gamba wallet so cashout doesn't work and balances don't show in wallet". Fixed by registering `"Gamba Network": "gam"` in NET_SHORT, adding the matching aliases in `core/framework/network.py::_ALIAS_TO_SHORT`, `database/users.py::_NET_FULL`, and `cogs/bank.py::_NET_NATIVE`, and migrating GBC + the eight game tokens into `wallet_holdings` on the `gam` short via new migration `0235_gamba_to_wallet_holdings.sql`. Every read/write path was updated to use `db.update_wallet_holding` / `db.get_wallet_holding`: `services/gamba.py` (stake / unstake / claim / cashout / award_game_token / yield credit / shop buy), `cogs/gamba.py` (`,gamba shop`, `,gamba buy`, balance probes), `cogs/chess.py` and `cogs/checkers.py` escrow paths (GBC bets), and `cogs/play.py` (new `_holding_get_raw` / `_holding_update_raw` dispatch helpers so any non-USD gambling bet on a Gamba token routes through the gam wallet, leaving the rest of the trading book on `crypto_holdings`). Also dropped the special-case `_CRYPTO_HOLDINGS_PARTNERS` exception in `services/buddy_economy.py` so BUD <-> Gamba burn-swaps now flow through the same `update_wallet_holding` dispatcher every other partner uses. Net effect for players: `,wallet create gam` works, `,wallet list` shows GBC and any held game tokens with their network breakdown, `,gamba cashout` reads from the same wallet the UI displays, and the Gamba Network finally feels like a real network instead of a special-case CeFi cluster. + +### Changes +- **Chess + Checkers boards now flip with the side to move and have a real coordinate gutter**: The PNG boards used to flip based on viewer identity, but in a public match the same message is shown to both players, so whoever made the most recent move ended up looking at their pieces upside-down on the next turn. Now `cogs/chess.py` and `cogs/checkers.py` derive `flip` from `board.turn` directly -- White / Red gets the unflipped view while it's their turn, Black gets the flipped view while it's theirs, and the orientation alternates automatically with every move. The viewer_id parameter is preserved on the public render helpers for caller compatibility but is no longer consulted for orientation. On top of that, `services/board_render.py` reworked `_draw_gutter` to render rank digits (1-8) and file letters (a-h) in the dedicated frame area outside the playable board (left and bottom gutters that were previously empty padding), at a larger 24pt size in the existing gutter foreground colour. Coordinates were technically drawn before but tucked into the inner corner of an edge square in the opposite-square ink, which was easy to miss; pulling them out into the gutter makes the board readable at a glance for picking move targets. + +### New Features +- **Per-position yield target on gamba game-token stakes (GBC default, BUD opt-in)**: A staked PIP / ACE / VEIN / EDGE / NOIR / CHERRY / GAMBIT / CROWN can now drip BUD instead of GBC, picked per-position. New `,gamba yield SYM ` flips a single stake; `,gamba yield all ` flips every active position; `,gamba stake SYM ` opens a fresh position straight on the chosen target. Both rates are 0.01 / token / day (`GAMBA_STAKE_BUD_PER_DAY`, parity with FREN's stake yield so emission stays balanced relative to the rest of the BUD economy). Mutually exclusive: each position picks one target -- a player who wants a split opens two stake positions over time, the same way FREN works. Crystallisation is built into both the explicit flip and the implicit one (passing a different `--target` on `,gamba stake`): the existing pending is paid out at the OLD target's rate before the row flips, so a player who'd been accruing GBC for 12h doesn't lose that on the switch. Service-layer cleanup ships in the same change: dropped legacy column / field names (`pending_gbc` -> `pending_yield_raw`, `gbc_paid_raw` -> `yield_paid_raw`, `accrued_gbc()` -> `accrued_yield()`, `total_accrued_gbc()` -> `total_accrued_yield()`); the `StakeRow` / `StakeResult` dataclasses now carry `yield_target`; the panel headline + APY labels read the row's actual target instead of hard-coding "GBC". `services/net_worth.py` values pending against each row's target oracle so leaderboards / tax / wealth-equalizer all see the same numbers. Migration `0234_gamba_stakes_yield_target.sql` adds the column with default `'GBC'`, renames `pending_gbc` -> `pending_yield_raw`, and indexes `(guild_id, user_id, yield_target)` so future per-target leaderboards are cheap. Existing rows default to GBC, so the deploy is zero-disruption for current players. Constraint stays application-side (`YIELD_TARGETS = {"GBC", "BUD"}`) so a future REEL / HRV target is config-only. +- **Circular buddy <-> gamba burn-swap loop -- BUD <-> all 8 game tokens**: Building on the new BUD <-> GBC pair, the eight Gamba game tokens (GAMBIT, CROWN, VEIN, PIP, EDGE, ACE, NOIR, CHERRY) are now bidirectional `,buddy convert` partners as well. This closes the full circle: a BUD holder can burn BUD for any specific game token (e.g. `,buddy convert bud pip 100`), stake it on the gamba surface for GBC drip, then convert GBC back to BUD whenever they want -- no USD round-trip required, no need to actually win that particular gamba game first to seed a stake position. Same dual-side oracle slippage and 1% LP kickback as every other BUD pair. Skipped the parallel "stake game tokens on the buddy network for BUD yield" idea on purpose: it would require either a new buddy-side stake table or a yield-target column on `gamba_stakes`, plus rewiring accrual / claim / panels / leaderboards, and the same goal (turn game tokens into BUD passively) is achieved by the existing chain stake game token -> claim GBC -> convert GBC to BUD. If the GBC -> BUD step turns out to be a real friction in practice, the parallel stake is a clean follow-up once we know whether it should replace or supplement the gamba stake. +- **Bidirectional BUD <-> GBC convert via `,buddy convert`**: Every other earn-only network coin (REEL, RUNE, MOON, FREN, HRV, BBT, INGOT) was already a partner with BUD on the buddy burn-swap surface, but GBC was missing -- there was no way to rotate gamba winnings into the rest of the earn economy without round-tripping through USD via the new cashout. GBC is now in `Config.BUD_SWAPPABLE_TOKENS`, so `,buddy convert bud gbc ` and `,buddy convert gbc bud ` work the same way every other BUD pair does (oracle-based, dual-side slippage, 1% LP kickback split across the two sides). The dispatcher in `services/buddy_economy.py` originally special-cased GBC because it lived in `crypto_holdings`; once migration `0235_gamba_to_wallet_holdings.sql` moved every Gamba token to `wallet_holdings`, the special case was dropped and GBC flows through the same `update_wallet_holding` path as every other partner. +- **Gamba Network gets a USD off-ramp via `,gamba cashout`**: GBC was previously a closed-loop network coin -- you could earn it from staking PIP / ACE / VEIN / EDGE / NOIR / CHERRY / GAMBIT / CROWN, and spend it at the Gamba Shop, but there was no way to convert it back to USD. New `,gamba cashout ` (aliases `burn`, `sellgbc`) burns GBC at the live oracle minus impact-based slippage and credits USD straight to the wallet, mirroring the existing `,fish cashout`, `,delve cashout`, `,farm cashout`, `,craft cashout`, and `,buddy cashout` flows exactly. Slippage IS the fee (no fixed haircut), the GBC oracle ticks down on the burn so the chart reflects the sell, and any LP holders of a GBC pool get a 1% kickback identical to the other earn-only network coins. Game-token stakes are not touched -- you can keep restaking PIP / ACE / etc. and cash out fresh GBC as it accrues. + +### Bug Fixes +- **Hotfix: bot startup crash from duplicate `withdraw` alias on `,gamba cashout`**: The previous commit registered `withdraw` as a third alias on `,gamba cashout`, but `,gamba unstake` already owns that alias. discord.py's `add_command` raises `CommandRegistrationError` on duplicate aliases and that aborted the entire `cogs.gamba` extension load, taking the bot offline. Dropped `withdraw` from `,gamba cashout`; the remaining aliases (`burn`, `sellgbc`) keep the surface discoverable. `,gamba unstake withdraw` is unchanged. +- **Chess + Checkers AI difficulty levels**: `,chess play` and `,checkers play` now accept a final `easy` / `normal` / `hard` argument so casual gamblers can actually win. Default stays `normal` (chess depth 2 / checkers depth 4 -- identical to today). Easy chess drops to depth 1 with heavy random tiebreak so the AI walks into hanging pieces; easy checkers searches depth 2. Hard tier looks one to two plies further than normal for grinders. Difficulty is persisted on the match row (migration 0233) and shown in the live board's meta strip ("AI **easy**") so the player remembers what they picked. PvP matches are unaffected. + +### Bug Fixes +- **Hotfix: client crash on every stake-panel message-update event**: `core/framework/staking.py` defined `StakePanelView._refresh(self)` as the panel's "re-render embed with latest state" helper, but `discord.ui.View._refresh(self, components)` is a discord.py-internal sync method that the gateway calls on every `MESSAGE_UPDATE` event for a tracked view (button click, embed edit, anything). Our async no-arg override shadowed it, so the next time Discord edited any open panel message the lib called `view._refresh(components)` and Python raised `TypeError: StakePanelView._refresh() takes 1 positional argument but 2 were given`, which propagated all the way out of the gateway loop and aborted the whole bot process. Latent for the life of `core/framework/staking.py`; surfaced now because the new `,gamba yield` flow + the BUD-target stake panel sends pushed several panels into channels that subsequently received message updates. Renamed our helper to `_redraw()` so discord.py's `_refresh(components)` no longer collides; the docstring spells out the rule so we don't reintroduce the conflict. Affects every economy panel that builds on `StakePanelView` (gamba, fishing, dungeon, farming, crafting, buddy). + +### Changes +- **Blackjack + Roulette upgraded to Pillow PNG rendering**: Following the chess + checkers redesign, `services/board_render.py` now also renders Blackjack and Roulette as proper PNG images so the visual quality is consistent across desktop, tablet and mobile. **Blackjack** renders an actual casino table: dealer hand on top with the hole card shown as a red diamond-pattern card back during play, your hand below, both with anti-aliased playing cards (rounded white face, suit + rank in the corners + a large centre suit, mirrored bottom-right). A banner overlay (BLACKJACK / YOU WIN / BUST / PUSH / DEALER WINS) is drawn on reveal. Suits cycle deterministically (♠♥♦♣) since the game logic is suit-agnostic. **Roulette** spins a real European single-zero wheel: 37 wedges in the canonical order (0-32-15-19-4-21-...), red / black / green colouring, a gold rim, the result wedge oriented to 12 o'clock with a white ball sitting on it, and a result strip below showing the spin number, colour and bet outcome. +- **Each `,play` game now has its own per-user lock**: Previously every game in `cogs/play.py` shared a single `(user_id, guild_id)` lock, so an in-progress Mines match blocked the player from starting blackjack, roulette, dice, slots, etc. until they cashed out or timed out. Each game's bet is already deducted upfront from the wallet, so a cross-game lock was over-conservative -- the financial guard already prevents over-betting. Lock key now includes `ctx.command.qualified_name` (e.g. `("uid", "gid", "play mines")`), so different games run concurrently per user but starting the same game twice is still rejected. PvP / single-game safety is unchanged. + +### Bug Fixes +- **Drop redundant Mines PNG attachment**: The previous commit attached a Pillow-rendered tile grid to the Mines embed, but Mines already renders its 5x5 grid as Discord's interactive button row -- buttons literally show the tile state and are the click target -- so the PNG below the embed was a duplicate visual that pushed the buttons off-screen on mobile. Reverted Mines to the original button-only layout (every send / edit no longer attaches a file, embed builders no longer set `image(attachment://...)`, the unused `render_mines_png` is removed from `services/board_render.py`). Chess, checkers, blackjack and roulette keep their PNG renders since none of those games use a button grid that already encodes the position. +- **Chess + Checkers boards rendered as chess.com-style PNG attachments**: Both ANSI and emoji-grid rendering had real problems -- ANSI colours (PR #770) got stripped on iOS / mobile Discord leaving an unreadable board, while emoji grids couldn't show actual chess pieces, just generic Unicode glyphs that read poorly on dark themes. New `services/board_render.py` uses Pillow to render proper PNG images server-side and attaches them to the embed via `attachment://`, so every Discord client (desktop, tablet, mobile) sees the same anti-aliased board. Chess uses chess.com's exact palette -- cream `#EBECD0` light squares, sage `#739552` dark, yellow `#F6F669` last-move highlight -- with stroked Unicode chess glyphs (white pieces filled white with black outline, black pieces filled black with cream outline) so pieces read on either colour. Checkers uses warm cream-and-walnut wood squares with glossy red and black checkers (3D shadow + highlight blob), yellow last-move highlight, and gold crown overlay on kings. Coordinate gutter is rendered inside the corner of each edge square in the opposite-colour ink, matching chess.com's labels. Pillow added to dependencies; DejaVu Sans Bold bundled in `assets/fonts/` so the renderer doesn't depend on system fonts being installed in the container. The previous text-board renderer (`core/framework/boards.py`) is removed -- no callers remain. + +### Bug Fixes +- **Fix Mines never showing the VEIN mint on the win embed**: `cogs/play.py` mines was the only game in the cog calling bare `set_tx(...)` on the result embed instead of `self._set_tx(...)`. The VEIN mint always landed in the player's wallet via `_apply_gamba_hooks`, but the "💎 Earned X VEIN" line that every other gamba game shows was silently dropped because only the `_set_tx` wrapper inlines the stashed `ctx._gamba_notes`. Players reasonably concluded mines didn't pay VEIN at all. Now uses `self._set_tx` so the Gamba Network field renders alongside cashouts and autowins, matching coinflip / dice / blackjack / roulette / slots. +- **Fix `,group lp deposit/withdraw/status` crashing with "No pool found for COOK/USD"**: `services/group_lp.py` was hard-coded to look up the founder's vault treasury action against a `TOKEN/USD` pool, but group tokens never get a USD pool by design -- `seed_group_genesis_pools` mints `TOKEN/mBTC`, `TOKEN/mSUN`, and `TOKEN/MOON` so trades route through real Moon-Network assets instead of a DSD shortcut. Every founder hitting `,group lp deposit ` got "No pool exists for {SYM}/USD. Ask an admin to seed one." even though the genesis pools were live, leaving the entire single-sided treasury->LP flow broken. Now resolves the pair against the founder's mining chain via the existing `wrapped_coin()` helper (mBTC for Bitcoin Network groups, mSUN for Sun Network) -- the same pair `create_vault_pool` uses -- and surfaces the actual pair on the status / deposit / withdraw embeds. Also exposed `services.group_lp.resolve_pair` so the cog can validate the chain binding before the confirmation round-trip. + +### Changes +- **Chess + Checkers boards redesigned for a uniform, professional look**: Both game embeds were rendering on visibly different systems -- chess used a plain monospace block with Unicode chess glyphs on a single bland background, checkers stacked emoji squares whose file caption never lined up under the columns. Players reported the pair "looked like a side project". New shared `core/framework/boards.py` module renders both boards inside Discord `ansi` code blocks with the same look-and-feel: cream light squares, rust dark squares for chess (navy for checkers, so red and gray pieces both keep contrast), an indigo last-move highlight, a gray-blue coordinate gutter, and bold filled glyphs in gold (white) / gray (black) for chess and red / gray for checkers. Cells are exactly three columns wide so rank digits on the left and file letters along the bottom land flush with their columns regardless of platform font. Embed bodies were also tightened: a single header line ("White to move · @user · Check!"), the board, then a one-line meta strip with bet, current move number, and recent SAN history; finished embeds promote the result banner to the top. The legacy emoji legend ("🔴 Red · 🚩 Red King · ⚫ Black · 🏴 Black King") is gone -- the glyph + colour combination is self-evident now. +- **Gameplay UI/UX overhaul -- production polish pass**: Comprehensive cleanup pass across the codebase before pinning up for production. Slimmed `cogs/hub.py` from 638 to 41 lines -- the standalone Hub view + embed builder were already retired (every `,today` invocation routes through the unified `,start` panel), so the legacy view classes, jump buttons, and parallel daily-claim path were dead weight bloating the cog. Removed the unused `core/framework/chain_confirm.py` (never imported anywhere -- `cogs/command_chain.py` defines its own confirmation view) and `core/framework/antibot.py` (a no-op that always returned True; deleted the six dead `if not await check_antibot(ctx): return` guards in `cogs/play.py`). Cleaned up every banned em / en dash in source (`cogs/groups.py` x5, `cogs/checkers.py`) per the CLAUDE.md hyphen-only rule. Replaced the lone hardcoded hex color in `cogs/earn.py` (`color = 0x000000 # black`) with a new `C_BLACK` constant in `constants/ui.py` so the wallet-drained ape outcome flows through the canonical palette. Refactored a batch of `to_human(int(row.get("col") or 0))` repetitions to the `row.h("col")` helper in `services/showcase.py`, `services/wealth_equalizer.py`, and `cogs/overview.py` so the raw NUMERIC(36,0) -> human conversion stays single-sourced. Dropped a stale "(SUN savings removed)" comment in `services/net_worth.py` -- the feature is gone, so the comment is just noise. No player-facing behavior changes; everything that was loading is still loading and reads cleaner now. + +### New Features +- **Interactive `,botinfo` dashboard with runtime sparkline charts**: New top-level command (and the `,help info` page) opens a multi-section view -- Overview, Runtime, Charts, Network, Services, Commands -- navigable via a dropdown plus Refresh / Dashboard buttons. Charts render Unicode sparklines from a new in-memory ring-buffer sampler (`services/runtime_stats.py`) that records gateway latency, process CPU, system CPU, RSS memory, system memory %, and guild count every 30s for the last hour. No PII or secrets are exposed; everything is read-only and ephemeral. Aliases: `,about`, `,uptime`, `,version`. +- **Admin command usage stats: `,admin commandstats` DM dump**: Every prefix-command invocation now writes one row to a new `command_usage` detail table plus a roll-up upsert to `command_usage_totals` from `Discoin.on_command`, so server admins can audit how active each game / feature is. New `,admin commandstats` (aliases `cmdstats`, `usagestats`) DMs the requesting admin a `.txt` file with three sections -- all-time totals (persisted across resets), last 7 days, and last 24 hours -- each grouped by qualified command path (top-level command + subcommand) with the most popular argument variants listed underneath. Migration `0232_command_usage.sql` provisions both tables; the totals table is keyed on `(guild_id, command_path, args_text)` so the cumulative count survives any future pruning of the detail rows. Discoverable via the new line under the 🩺 Diagnostics admin help category. + +### Bug Fixes +- **Fix CeFi trade burn never reducing supply for built-in tokens**: `cogs/trade.py` was calling `update_circulating_supply` for the burn portion of every buy and sell. That helper only touches the `guild_tokens` table, so for the eight built-in tokens (BTC, ETH, USDC, AAVE, SUN, DSC, DSD, DSY -- everything in `Config.TOKENS` with a `burn_rate`) the burn was a silent no-op even though embeds advertised `🔥 Burned X tokens (0.x%)`. Mirror the dispatch pattern from `services/swap.py`: call `update_builtin_circulating_supply` for `Config.TOKENS` symbols and `update_circulating_supply` for custom guild tokens. Built-in tokens now actually deflate over trade volume, matching their config'd burn rates. +- **Fix CeFi buy embed showing pre-impact burn while crediting post-impact qty**: The buy path computed `_buy_burned = qty * burn_rate` and `_buy_fee_tokens = qty * xfer_fee` against the spot-price quantity, then re-derived `qty` against the slippage-adjusted effective price and recomputed `qty_after_contract` -- but left the burn/fee variables stale. Result: every buy with non-trivial slippage showed the user a "🔥 Burned" line, a `📋 Contract Fee` line, and a transfer-fee USD reserve credit all calculated off the pre-impact qty, while the actual `qty_after_contract` debit they got was based on the post-impact qty. On a 5%-impact buy with 1% burn the embed over-stated the burn and the reserve credit by ~5%. Moved burn/fee computation to after the slippage step so display, supply burn, and the user's actual fill agree. +- **Fix admin `,admin give` / `,admin take` double-counting custom-token supply**: Both commands called `update_holding` (which already auto-syncs `guild_tokens.circulating_supply` for custom tokens and `crypto_prices.circulating_supply` for built-in tokens) AND then `update_circulating_supply` (which only touches `guild_tokens`). Result: for a custom guild token, every admin grant double-incremented circulating supply and every admin take double-decremented it, drifting the supply field away from the sum of holdings over time. Built-in tokens were tracked correctly by accident because the second call was a no-op for them. Removed the redundant second call -- `update_holding` already covers both tables in a single pass. + +### Discord Bot +- **Fix ,mystakes crash for Safety Module holders**: cogs/stake.py:2699 was unpacking _sm_pending_yield into 2 values:; _pending_h, _daily_h = await self._sm_pending_yield(ctx, _sym); The function returns a 3-tuple (pending_h, daily_h, is_auto_compound) (`0d1b919b`) +- **Fix ,economy dashboard scaling + ,wealth flow crash**: Five real bugs surfaced once the dashboard started getting traffic:; - Money / Trading / Gambling tabs: every monetary read was a raw; NUMERIC(36,0) (10^18-scaled per migration 0075). fmt_usd printed (`47d37b3d`) +- **Fix startup crash: drop 'wealth' alias from ,balance**: cogs.wealth_equalizer registered a ,wealth group and bot startup; crashed because ,balance already had 'wealth' in its alias list:; CommandRegistrationError: The command wealth is already an existing (`aa489136`) +- **Closed-loop game-token sinks + adaptive faucet + inflation telemetry**: Three soundness additions in one branch, all sharing the existing; wealth-equalizer + cached_bulk_net_worth infrastructure:; 1. Closed-loop sinks for the seven earn-only network coins (`2f811061`) +- **Communicate the equalizer/throttle stack and purge legacy scaling text**: The new wealth equalizer + whale yield throttle was live in code but; invisible to players: ,help still talked about "GDP Scaling", ,daily; showed a "Supply scaling" line that was no longer accurate, ,work and (`90213c45`) +- **Whale yield throttle on every passive surface + Health dashboard**: The wealth tax is a one-shot drain; without a flow-side brake whales; compound back to dominance from staking + savings + LP yield within a; single tax cycle. Add a shared yield-multiplier curve and apply it (`c114ff66`) +- **Wealth Equalizer: surface where the taxes go**: Players couldn't see the flow -- ,wealth lumped everything as totals and; the per-cycle breakdown was hidden. Add three new flow surfaces and; expand the auto-announce to name names: (`ce23ec05`) +- **Add Wealth Equalizer: daily progressive wealth tax + UBI redistribution**: Top-of-book stablecoin distribution had ~97% of wealth pooled in <10; players. Add a daily redistribution loop:; - services/wealth_equalizer.py: bracket-marginal tax on liquid stable (`5869108a`) +- **Simplify groups: drop LP kill switch, hide duplicate pool deposit/harvest, fix mine help**: Three pieces of bloat removed.; 1. LP kill switch (,group lp enable / disable) -- gone.; The treasury_lp_unlocked column gated every LP deposit/withdraw (`2d1301a6`) +- **,group help: rewrite every category to actually teach the system**: Players were bouncing off ,group help because each category was a; flat list of commands with no narrative -- you had to already know; how a mining group worked to make sense of it. Rewrote seven of the (`d47e5367`) +- **,group help LP: spell out that BOTH partnership founders can keep adding LP**: User read the previous LP help and thought only the original; proposer / accepter could deposit into a partnership pool. The; underlying impl already supports either-founder deposits -- (`cada2fc6`) +- **,group help LP: explain how LP actually works with worked examples**: Previous LP help listed commands but didn't explain liquidity pools; in plain English -- a founder reading it had to already know what; "single-sided", "cost basis", and "slippage" meant. Now the card: (`9b2a7d68`) +- **Consolidate group LP commands under ,group lp; explain funding sources**: User confused by the split between ,group pool deposit (USD-from-; reserve, double-sided) and ,group lp deposit (pct-of-vault, single-; sided). Both are LP ops, both should live next to each other in (`edad52fa`) +- **,group help: surface the LP Treasury commands in their own category**: User asked which command adds liquidity into their group's LP pool; as the founder. The commands existed (,group lp status / enable /; disable / deposit / withdraw ) but they weren't listed (`ddbd137e`) +- **Allow game commands in group halls; add ,gamba stake autocompound bulk toggle**: Two fixes:; 1. ,move (and every other game command) was getting blocked in; group hall threads when the guild had bot_channels configured. (`f376be4a`) +- **Stake all / unstake all + net worth + balance integration**: Three pieces:; 1. ,gamba stake all / ,gamba stake everything; Iterates all eight game tokens, stakes the full wallet balance of (`13361b87`) +- **Checkers board: rank/file coordinates + recolour the king emojis**: User feedback: the board was unlabeled, so referencing a square; ("a3", "d2") meant counting columns from the dropdown name. Also; the red king emoji 🟥 (Large Red Square) was rendering as (`01e8d7f8`) +- **Add Bump + Auto-bump to chess/checkers; surface them in /games + ,play**: User feedback: chess and checkers matches scroll out of sight as the; channel fills with chat / other commands; the panel needs a way to; re-post itself at the bottom on demand AND automatically when the (`8a5b795f`) +- **Inline Gamba mint on the gambling-game embed; rename checkers piece labels**: Two fixes:; 1. Token mint / Lucky Chip / House Marker results now render as a; "🎰 Gamba Network" field ON the win-or-loss embed of dice / cf / (`a6f41890`) +- **Restore chess Unicode glyphs, switch checkers to emoji-per-cell board**: User feedback: ASCII letter pieces ("R", "b") looked terrible. Chess; was readable with the original Unicode chess piece glyphs (♔♕♖♗♘♙ /; ♚♛♜♝♞♟); checkers needs to feel like checkers, not a math notation. (`e2662feb`) +- **Chess + checkers: button UX, clean ASCII boards, mint follow-up**: User feedback: the checkers board was unreadable (mixed emoji widths; collapsed columns), there were no buttons, and the existing 6 gambling; games never surfaced the themed-token mint to the player. (`5110f8c9`) +- **Fix chess startup crash: alias collision + chess pkg build pin**: Two production bugs from the Railway deploy logs:; 1. ,chess aliased to "ch" collides with cogs/challenges.py:147 which; already owns "ch" -- bot crashed at setup_hook with (`416d96f0`) +- **Add Gamba Network: chess, checkers, 8 game tokens, GBC, shop**: The gambling surface is now its own closed earn-only network alongside; Lure / Crypt / Buddy / Harvest / Forge. All eight games (chess, checkers,; mines, dice, coinflip, blackjack, roulette, slots) mint a themed earn- (`b99f27cb`) + +### Changes +- **Add chess to uv.lock so Railway uv sync --frozen picks it up**: Dockerfile runs ``uv sync --frozen`` from uv.lock, not pip from; requirements.txt. The pyproject.toml + requirements.txt entries from; the previous chess pin commits were ignored at build time because (`4b06361a`) + +### Services +- **Add 1478968538579337460 as auto-unlocked dev guild**: HOST_GUILD_ID was a single-server gate -- only the operator's home; server (1467740704725012638) got every premium feature unlocked; without a DB row. Add a parallel Config.DEV_GUILD_IDS frozenset that (`f1687b9c`) + +--- + +## [main] -- 2026-05-07 + +### Bug Fixes +- **Fix bot startup crash: `,wealth` collided with `,balance` alias**: The new wealth-equalizer cog registered a `,wealth` group, but `,balance` already had `wealth` in its alias list (`bal`/`bals`/`wealth`/`net`/`networth`/`p`), so cog load aborted with `CommandRegistrationError: The command wealth is already an existing command or alias`. Dropped `wealth` from the `,balance` alias list -- the redistribution group owns the name now, and `,balance` still has five other shortcuts (`bal`, `bals`, `net`, `networth`, `p`) so existing balance-checking habits still resolve. +- **Fix every monetary value on `,economy` reading raw NUMERIC(36,0) and rendering as quintillions**: The economy dashboard was printing `users.wallet`, `users.bank`, `loans.outstanding`, `transactions.amount_in` (24h volume), `pow_network_state.current_reward`, validator stake totals, and `transactions.amount_in` for gambling wagers all at raw 10^18-scaled magnitudes -- `fmt_usd` printed them verbatim, so a $10k server wallet showed as `$10,000,000,000,000,000,000,000.00`. Every monetary read on the Money / Trading / Mining / Gambling tabs now goes through `to_human()` at the display boundary. +- **Fix Mining tab block reward always reading 0**: Cog was reading `ch.get("block_reward")`; the actual `pow_network_state` column is `current_reward` (raw NUMERIC(36,0) per migration 0075). Switched to the right column + descaled. +- **Fix Staking tab "0 staked" for every validator**: Cog was reading `validator["total_staked"]`, but the `validators` table has no such column -- per-validator stake totals have to be aggregated from the `stakes` table. Added a single bulk `GROUP BY validator_id` aggregation, descaled via `to_human`, and used the aggregate to also rank the "Top Validators" list correctly. PoS `stake_amount` was raw too -- now descaled. +- **Fix Gambling tab "0 games / 0 wagered" even after live games**: `get_economy_snapshot` was counting `game_results` rows, but live `,play` / `,gamba` writes go to `transactions` with `tx_type LIKE 'GAMBLE_%'` (cogs/play.py:1081). The `game_results` table is API-router-only and effectively empty, so the dashboard always reported zero. Switched the snapshot query to `transactions` and descaled the bet sum. +- **Fix `,wealth flow` crash with epoch-float `cycle_at` against TIMESTAMPTZ bind**: `get_cycle_summaries` returns `cycle_at` as the DB-coerced epoch float (CLAUDE.md), but `get_cycle_detail` re-bound it directly into the next query, which asyncpg refused with `invalid input for query argument $2: ... (expected a datetime.date or datetime.datetime instance, got 'float')`. Added a `_to_dt` coercion helper. Same hazard fixed in the Health-tab trend code that was comparing the snapshot timestamp to a Python `datetime` cutoff. +- **Fix `,mystakes` crash "too many values to unpack (expected 2)" for Safety Module holders**: Pre-existing 2-tuple unpack at `cogs/stake.py:2699` predated the addition of the `is_auto_compound` return value; the function had since become a 3-tuple but this one call site missed the update, so any user with an AAVE / DSY position couldn't open `,mystakes`. The other call site at line 3771 already unpacked 3 correctly. Fixed the unpack and surfaced "🔁 Auto-compounding" status in the panel when auto-compound is on. + +### New Features +- **Wealth Equalizer: daily progressive wealth tax + UBI stipend**: Brand-new redistribution loop targets the top-heavy stablecoin distribution where a tiny minority of players were sitting on ~97% of the supply. Once a day per guild, a marginal-bracket tax ladder drains liquid stablecoin holdings (wallet -> bank -> USD savings) above a $50k floor (0.5% / 1.5% / 3% / 6% / 10% per bracket) into a server-owned redistribution pool, then pays the pool out as a flat $50 - $5,000 UBI stipend to every active player whose net worth is under $25k. Ships with `,wealth` (pool, brackets, last cycle), `,wealth me` (your tax/UBI history), and bot-owner `,wealth runnow` for out-of-band cycles. Public recap embed posts to the announcements/events/drops channel after each cycle so players see the redistribution land. Net worth is read via the canonical `compute_bulk_net_worth` so the bracket the player sees matches every other balance surface in the bot. +- **Wealth Equalizer flow visibility**: New `,wealth flow` shows the most recent cycles as `taxed -> UBI -> pool carry` arrows with the top 3 payers + top 3 recipients of the latest cycle named with mentions. New `,wealth top` lists the all-time top 10 tax payers (who funds the pool) and top 10 UBI recipients (who lives off it) with cycle counts. The auto-announce embed now leads with the same `taxed -> UBI -> pool` arrow and surfaces top 5 payers + top 5 recipients per cycle so players can see exactly which accounts the redistribution moved money between, not just aggregate totals. +- **Whale yield throttle on every passive income surface**: The wealth tax was a one-shot drain; without a flow-side brake, whales would compound back to dominance within a single tax cycle from staking + savings + LP yield. New shared yield-multiplier curve scales every passive payout by the player's current net worth: 100% below $50k, 70% at $1M, 35% at $10M, 15% at $100M, log-decaying toward a 10% floor past that. Wired into savings interest tick (`cogs/bank.py`), validator + delegator block rewards (`cogs/stake.py`, with throttled excess flowing back into treasury so the network keeps the captured value), LP yield distributions (`services/lp_yield.py`), and the active-income surfaces with the highest abuse potential -- `,daily` and `,work`. Net worth is read once per guild per tick from a 5-minute cache in `services/wealth_equalizer.cached_bulk_net_worth`, invalidated whenever a tax cycle finishes so the post-tax world is reflected immediately. Curve is fully `Config.WEALTH_YIELD_THROTTLE_CURVE` driven and the `,economy` Health tab renders it live. +- **,daily wealth scaling now uses true net worth, not wallet+bank**: The pre-existing `DAILY_SCALING_ENABLED` only saw liquid stablecoin balances, so a whale with $50M in stakes / LP / farms farmed full daily because their wallet+bank was empty. Switched to the canonical `compute_bulk_net_worth` snapshot so the scaling sees every asset class the equalizer sees. Stacked with the new whale-throttle multiplier for an absolute floor on top of the existing relative `boost-the-poor / nerf-the-rich` curve. +- **,economy Health tab: Gini, top-1/8/25 concentration, redistribution flow**: Brand-new tab on the `,economy` paginator surfaces the soundness state at a glance -- Gini coefficient with a healthy/moderate/high/severe/extreme grade, top-1/top-8/top-25 net-worth concentration as percentages, P50/P90/P99 net-worth percentiles, the live redistribution pool, last cycle headline numbers, and the active whale-throttle curve so admins can see exactly how the brake is calibrated. All metrics share `cached_bulk_net_worth` with the equalizer + throttles so cross-screen numbers always agree. + +### New Features +- **Closed-loop sinks for the seven earn-only network coins**: REEL / RUNE / HRV / FORGE / BUD / DFUN / GBC are minted continuously by per-game stake-yield ticks while the shipped sinks (gear, slots, conversions) are fixed-price one-shots, so circulating supply was running away -- per the audit, GBC + RUNE were critically under-sunk and DFUN was high-risk. New `services/token_health.py` adds an `audit_token` helper and a `game_token_burn_phase` that runs as a third phase of the daily wealth-equalizer cycle: every holder above the configured threshold has the marginal excess burned at the per-token rate, and `crypto_prices.circulating_supply` is decremented to match so the burn is a real sink (not a transfer). Defaults: GBC/RUNE 5%/cycle above threshold, DFUN 3%, HRV/FORGE 2%, REEL 1%, BUD exempt (already well-sunk). Burns log into `wealth_redistribution_log` with `kind='token_burn'` so `,wealth flow` and `,wealth top` surface them alongside the USD tax + UBI rows, and the auto-announce embed lists per-token burn totals in a "🔥 Game-Token Burn" field. New "🔥 Game-Token Health" page on `,economy` shows per-token circulating supply, holder count, top-holder concentration, and the active burn rate for every game coin. +- **Adaptive faucet: payouts auto-scale with per-active-player supply**: The auto-faucet's "GDP-scaled" comment was misleading -- the only knob was a static admin multiplier. Real adaptive scaling now reads `services.wealth_equalizer.get_supply_per_active` and computes `clamp(REFERENCE / (REFERENCE + per_capita), MIN_MULT, MAX_MULT)`. A poor server (per-capita << reference) keeps generous drops up to **x3.00**; a supply-heavy server (per-capita >> reference) dials them back to **x0.20**. Stacks multiplicatively with the admin `faucet_multiplier` override so operators retain final say. Visible on `,economy` -> Health tab as the live multiplier with the per-capita reading. +- **Inflation telemetry: 24h + 7d trend deltas on the Health tab**: New `economy_health_snapshots` table (migration 0231) stores per-guild distributional metrics every 6 hours via a `cogs/wealth_equalizer.snapshot_task`. The `,economy` Health tab now renders **24h Trend** and **7d Trend** fields with up/down arrows for total supply (raw + %), Gini (delta), and top-8 concentration (delta in points). Lets operators watch the equalizer + throttle + sinks actually move the curve over time without scraping logs. The snapshot loop runs even if the equalizer itself is disabled, so operators can run telemetry-only. + +### Documentation +- **Help + embed swap for the new equalizer/throttle stack**: New `,help wealth` category covers the daily wealth tax, UBI stipend, the whale yield throttle curve, and where to read soundness telemetry. Aliases: `tax`, `equalizer`, `ubi`, `redistribution`, `throttle`, `inequality`, `gini`. Updated entries: `,help daily` (drops the legacy "GDP scaling" line, adds the new wealth-curve explanation with a `,help wealth` link), `,help savings` / `,help staking` / `,help pools` (each now explains the throttle as it applies to that surface), `,help economy` (links to `,wealth` + the Health tab), `,help` overview (lists `,help wealth` under a new "Soundness" heading). Reply embeds: `,daily` renames the legacy "Supply scaling" line to "⚖️ Wealth curve" and shows the actual multiplier; `,work` adds a "⚖️ Wealth curve" earnings line showing the multiplier and dollars trimmed when throttle is active; `,bank savings` adds a "Wealth Curve" field that shows the player's exact bracket position, the effective post-throttle APY, and the daily-yield estimate using the throttled rate. Admin + user docs (`docs/admin-guide/economy.md`, `docs/user-guide/economy.md`) replace every legacy DAILY_SCALING / GDP-scaling explanation with the new model and document every config knob (`WEALTH_TAX_BRACKETS`, `WEALTH_UBI_*`, `WEALTH_YIELD_THROTTLE_CURVE`, `WEALTH_YIELD_THROTTLE_FLOOR`). +- **Gamba Network: chess + checkers + 8 game-themed earn tokens + Gamba Shop**: Brand-new closed earn-only network alongside Lure / Crypt / Buddy / Harvest / Forge. The network coin is GBC (Gamba Coin); each gamba game mints a themed earn-only token on every win (chess -> GAMBIT, checkers -> CROWN, mines -> VEIN, dice -> PIP, coinflip -> EDGE, blackjack -> ACE, roulette -> NOIR, slots -> CHERRY). Stake any game token to passively drip GBC at the same fixed daily rate fishing's LURE -> REEL uses (0.01 GBC per token per day). Spend GBC at the new ,gamba shop on three single-use consumables -- Lucky Chip (+5% on next win), House Marker (25% bet refund on next loss), and Side Bet Slip (2x token mint on next win), all auto-applied. New ,chess and ,checkers commands ship with vs-AI mode, PvP challenges, USD/GBC bets, ELO leaderboards, and per-user record cards. + +### Configuration +- **Quadruple Disc.Fun stake yield, double underlying volatility**: Bump staking emission, cap, floor, and legacy fallback APY 4x in; Config.DISCFUN so fun stakes pay out four times as much DFUN at every; TVL level (the live emission rate scales linearly with emission, and (`ef4f4f08`) + +### Discord Bot +- **Fix ,fun buy SYM all erroring 'Insufficient DFUN balance' from float overshoot**: The 'all' / 'max' / 'X%' paths convert the raw DFUN holding to a float; via to_human, then _execute_buy round-trips the float back to raw via; to_raw. float64 can't represent every 18-decimal raw int exactly, so (`3c087ceb`) +- **Surface beachcomb/scavenge on ,today home tab + fix phantom cricket bait**: Home-tab buttons:; - services.hub._ready_to_claim_hints probes last_beachcomb_at and; last_scavenge_at via DB-side EXTRACT(EPOCH FROM (NOW() - col)) the (`bf008597`) +- **Fix buddy AH listing, surface scavenge/beachcomb on ,start, add storage commerce**: - find_owned_buddy_token lazy-mints item_instances when only cc_buddies; has the buddy, so brand-new buddies can be listed without erroring; with "Couldn't find buddy #N's NFT" (#384) (`9affebb9`) +- **Fix stale Pray/Open-Chest buttons on ,start home panel**: After clicking Pray or Open Chest on the ,start home tab, the panel; was not refreshed, so the one-shot button stayed visible. A second; click produced a confusing "No shrine here" / "No chest here" error. (`0617d872`) +- **Fix nest collect crash + Disc.Fun stake-button user_row crash**: * services/buddy_breeding._return_parents was passing the raw asyncpg; Connection to user_max_battle_slots / user_max_storage_slots, which; in turn call ensure_state(db) -> db.fetch_one (a PgDatabase wrapper (`d5e7fc75`) +- **Add 3-tier item browser with AH listing, stack drilldown, junk-sell**: Replace the single-dropdown ,items browser with a Category -> Item; Stack -> Item navigation flow plus a dedicated action-button row.; * Tier 1 (kind) -- existing category filter, kept intact (`3d12adcd`) +- **Wire the third crafting-specialty slot through every display surface**: The third slot already worked on the service side -- add_specialty; computed cap = ACTIVE_SPECIALTY_CAP + extra_specialty_slots, and; the shop buy debited USD + bumped the column atomically. But three (`1f237d5c`) +- **Fix ,group pool harvest burning entire LP, add deposit recovery path**: * Migration 0224 adds group_lp_positions.cost_basis_usd_raw and; backfills it from each active position's current pool value.; * New harvest_group_lp_fees_only helper computes USD gain over (`8d27c154`) +- **Disc.Fun: variable APY, stake overview + buttons, admin commands, Moon-Network fix**: * Migration 0222 force-adds discfun_stakes.auto_compound /; total_compounded for hosts that ran the early draft of 0218; (which created the table without those columns) -- fixes (`d1ae110a`) +- **Disc.Fun: custom emojis, edit command, 7-day inactivity sweep, chart scale fix**: * Deploy regex + validate_emoji now accept Discord custom emojis; (<:name:id> and animated ) alongside unicode glyphs.; String is stored verbatim and discord.py renders it inline. (`45a55a65`) +- **Fix buddy nest inventory crowding and Disc.Fun curve-cap deadlock**: * Buddies deposited in the nest now flip to a new status='nesting'; (migration 0220) so they no longer count against the battle or; storage slot caps. Collect / cancel routes parents back to active (`85e24a8e`) +- **Eggs button pivots in-place + paginated db browser**: Buddy storage <-> egg picker:; * The Eggs button on `,buddy storage` swaps the message embed to the; egg picker (same chat slot) instead of dumping a stub redirect. (`6736bd50`) +- **Hide delve/buddy/fishing/crafting action receipts behind ephemeral=True**: Pickaxe Strike, shrine Pray, escaped-buddy battle opening + win/loss; results, buddy-AH listing confirmation, crab-haul, trap-set, egg-hatch,; fishing tackle panel, and craft-success receipts were all posting as (`6ec6c27a`) +- **Multi-slot buddy nest, hidden egg rarity, delve fixes (#372/#373/#374)**: * New: buy up to 9 extra nest slots (`,buddy slot nest buy`, max 10; total). cc_buddy_daycare migrates to a serial id PK so multiple; parallel deposits coexist; `,buddy nest` lists every slot, collect (`52ccafc6`) +- **Surface silent delve buffs in the combat log + chest receipt**: resolve_attack was applying wildshape, shrine_atk, shrine_spd, sanctuary,; thorn_aura, and regen silently -- multipliers folded into ATK/SPD/incoming; damage with no log line, so a buffed-up player just saw "you hit for X" (`a5b99521`) +- **Add random surprise events to ,work and ,daily**: Both commands now have a small chance of rolling a positive; surprise on top of the normal payout (~2.5% on work, 3% on daily).; On hit, randomly picks one of: (`aeaa3c9d`) +- **Fix ,dev status CardBuilder.to_dict crash: missing .build() in module health helper**: `_build_module_health_embed` was declared `-> discord.Embed | None` but; returned the bare CardBuilder from `card(...)` without calling `.build()`.; Both ,dev status (Game Systems page) and the auto-status DM appended the (`e297d5fb`) +- **Fix .dev status Severity UnboundLocalError + sextillion-dollar security alerts**: cogs/dev.py:; - The doctor-scan section did `from cogs.health import _SEV_ICON, Severity`; inside both dev_status and auto_status_dm. dev.py already imports (`d7c86085`) +- **Rework .dev status into next-gen report; wire heal/doctor in; fix NULL-as-disabled bug**: The previous pass reworked the auto-status DM but left ,dev status alone; and shipped a regression: my new module health embed read each; guild_settings.module_* flag with `dict.get(col, True)`, which returns (`49137d7f`) +- **List ,fish beachcomb and ,delve scavenge in their help pages**: The two free-roll commands shipped earlier but were missing from; ,fish help and ,delve help, so players couldn't discover them; without reading the changelog. Both help pages now include the (`09363d9d`) +- **Rework status reports: fix quintillion-dollar TVL, stale heartbeat keys, prefix mismatch**: cogs/status.py:; - Pool TVL summed reserve_a/_b raw, but pools.reserve_* are NUMERIC(36,0); scaled by 10**18, printing $quintillion totals on every server. Descale (`174310a1`) +- **Fix startup crash + relic contract deploy (production bugs)**: Two critical bugs from the Railway deploy logs:; 1. Bot crash on startup: my new ,delve scavenge aliased; 'salvage', but ,delve junk already owns that alias. discord.py (`87ed8a24`) +- **One-fight-at-a-time blocker for buddy/wild/farm fights**: Previously a player could start a buddy PvP, then run ,delve battle,; then click Challenge on a fish wild buddy -- three fight surfaces; resolving against the same active buddy / HP, with battles writing (`d8bae296`) +- **Add ,fish beachcomb + ,delve scavenge -- farm forage siblings**: Two new free-roll wander commands modelled on ,farm forage. Same; shape and feel: free roll, 10-minute DB-clock cooldown, send-then-; edit animation with a pre-frame and a per-outcome reveal frame, (`847c4f56`) +- **Rewrite group leaderboards: rank by what the group actually owns**: The old ,group lb reserves only summed treasury cash columns, and; the old ,group lb mktcap measured CIRCULATING SUPPLY (a property; of the token, not "what does this group own"). Both wrong. (`8d1557b9`) +- **Fix ,stake all off-by-1 + add createpool, groups fixpools, group lbs**: Bug fixes; - ,stake aave all / ,stake dsy all stopped tripping "have X but; need X" off-by-1. The Safety Module deposit path was doing (`ef8e3a33`) +- **Scrub remaining premium leaks, add owner recovery + welcome + tests**: Cost-leak gap from the first premium pass:; - cogs/farming.py: gated, was paying AI-equivalent cost as a buddy; minigame in the same bucket as fishing/crafting/delves (`8328e257`) + +### Bug Fixes +- **Add 'auction' to cc_buddies status check constraint**: services/auction.py sets status='auction' when escrowing a buddy into; the AH, but migration 0220 rebuilt cc_buddies_status_chk with only; ('owned','shelter','stored','nesting') and left 'auction' out. (`b029a0f0`) +- **Migration 0219 -- bigint overflow on 10M * 1e18 literal product**: Production bot was crash-looping on startup with:; asyncpg.exceptions.NumericValueOutOfRangeError: bigint out of range; at /app/database/database.py:303 (await conn.execute(sql)) (`fe40d8ef`) +- **,fun price formats -- subscript-zero notation + USD equivalents**: A fresh proto's price was rendering as `1.4423e-06 DFUN` because the; formatter fell through to Python's default scientific notation for any; value below 1e-4. Memecoin prices live in that range, so every (`e0ac7a95`) +- **,fun panel buttons crashing on parent attribute (read-only in discord.py)**: Same family of bug as the recent _DbBrowseView._snapshot regression --; discord.ui.Item makes `parent` a read-only property in newer discord.py; builds, so the FunPanelView button constructors crashed on every (`7b756ed2`) +- **Trim fish trap place slash description to under 100 chars** (`9dd8daa9`) +- **Safety Module APY floor 50%, emission raised to $50k/day**: emission_usd_per_day: 500 -> 50,000 so the curve stays generous; at realistic TVL (was 0.8% at ~$22M TVL, now ~80%+).; Added min_apy_pct: 50.0 to config and sm_current_daily_rate() (`a4e4c76f`) +- **Clarify flee stays in dungeon, fix scavenge error message**: flee only escapes the current mob fight -- run_id stays set and; the player is still inside the dungeon afterward. The scavenge; error incorrectly said ",delve flee your current run" implying (`4334e012`) +- **Silent auto-pump, 1-60 min random interval, delve tether, db kind fix**: - remove auto-pump channel announcements entirely; - change interval window to 60-3600 s (1-60 min) per guild, randomly; re-rolled after each fire (`71c5ba46`) +- **Combine buddy stake-all reply and fix db browse _snapshot crash**: - buddy stake everything now sends one embed with both FREN and BBT; sections instead of two separate receipt messages; - rename _DbBrowseView._snapshot -> _browse_snap to avoid the (`bce19d04`) + +### Database +- **Migration 0225: tie user_crafting active-specialty CHECK to extra_specialty_slots**: Migration 0172 added the CHECK at hardcoded <= 2. Migration 0179; later introduced extra_specialty_slots but never relaxed the CHECK,; so any player who bought the third-slot unlock and tried (`c843fb3f`) + +### New Features +- **,fun chart -- Disc.Fun bonding-curve candlestick charts**: Proto tokens get their own candlestick chart command + panel button.; The rendering pipeline is identical to ,chart (lightweight-charts in; headless Chromium via playwright), but the source template is a new (`ab997455`) +- **Lower Disc.Fun graduation to 10M DFUN + ,fun buy all/% parsing**: Two fixes in one push.; Graduation 50M -> 10M DFUN; -------------------------- (`6da4d907`) +- **,fun deploy rate-limit -- 1 launch per user per server per 24h**: Players were free to spam protos from a single account, which would; crowd the launchpad and drown out every other token. This ships a; 24h-per-(guild_id, creator_id) rate gate. (`4622697a`) +- **,fun autocompound toggle on Disc.Fun stakes**: Staked positions can now flip into autocompound mode:; ,fun stake SYM autocompound [on|off|toggle]; ,fun autocompound SYM [on|off|toggle] (alias) (`7b8dcba8`) +- **,fun stake/unstake/claim/stakes + $5M USD graduation threshold**: Disc.Fun staking; ----------------; Holders of graduated proto tokens can now stake them back into Disc.Fun (`74240eab`) +- **,balance summary row + dropdown tab for Disc.Fun**: Adds Disc.Fun to the ,balance Summary holdings list (sorted by USD value; alongside CeFi / DeFi / Nodes / etc.) and a dedicated 🎢 Disc.Fun tab in; the dropdown that lists every active proto position with current spot, (`3a2b1cd2`) +- **,fun bag/trade USD totals + Disc.Fun in net worth**: - ,fun bag now shows Value / Cost / PnL on three lines, each with a; live USD equivalent. Buy / Sell receipt embeds also append; ($X.XX) to the Paid / Received / Fee fields and to the New Price / (`a1ec9133`) +- **Disc.Fun ,fun command + DFUN quote token + interactive button UI**: - Rename ,discfun to ,fun (aliases df / discfun) so the command isn't the; "horrible long thing" it was. ,fun also opens a richer overview panel; showing hot protos, recent graduates, and the full command set. (`681ea636`) +- **Disc.Fun proto-token launchpad (Pump.fun replica)**: Anyone can now ,discfun deploy a proto token on the Discoin Network with; a flat 50 DSD fee. Each proto trades against a fixed virtual-DSD bonding; curve (1B supply, 800M on the curve, 1% fee) and graduates automatically (`bcff451f`) +- **Add ,fish trap place all command**: Places all owned traps eligible for the current zone in one shot.; Highest-tier traps get priority when the 8-trap cap is tight; traps; whose max_zone_tier is below the current zone tier are skipped with (`3a9fa2b7`) +- **Safety Module variable APY (up to 10,000%) + auto-compounding**: Emission-based dynamic APY: $500/day emitted to all stakers per symbol.; Rate = emission / TVL, capped at 10,000% -- high when TVL is tiny, drops; as more is staked (Cetus-style). Replaces the hardcoded daily_yield=0.00137 (`3e96efed`) + +### Maintenance +- **Reword Disc.Fun graduation as 50M DFUN flat (drop USD framing)**: Numeric value is unchanged at 50,000,000 DFUN -- this is purely a; framing pass on the config comment and CHANGELOG entry so the; milestone reads as a clean "50M DFUN" target rather than indirectly (`af6246de`) + +### Frontend/UI +- **Fix discoin.cc tabs rendering as a useless thin column**: components/ui/tabs.tsx targeted Tailwind `data-horizontal:` /; `data-vertical:` selectors -- but Base UI's tabs primitive sets; `data-orientation="horizontal"|"vertical"`, never `data-horizontal`. (`dadf1444`) + +### Services +- **Backfill + lazy-mint find crafted-as-consumable items**: anglers_paste, expedition_ration, and every other crafting recipe; whose output ``apply: consum/`` lands in; user_dungeon.consumables couldn't backfill: the loop only looked (`fc73aadd`) +- **Make every dungeon item AH-listable (contracts + lazy mint + kinds)**: Three compounding bugs trapped dungeon items in player inventories:; 1. dungeon_config.JUNK (mats / salvage like enchanted_thread,; runed_chip, dragon_scale) and dungeon_config.RELICS were never (`f6017854`) + +### API Changes +- **Premium phase 3: gift cmd, staff_audit, admin REST API, host default**: - HOST_GUILD_ID defaults to 1467740704725012638 in config.py so the; operator's home server is auto-unlocked even before any env var; is set. Self-hosted deployments still override via .env. (`f9f205b6`) +- **Add per-guild premium subscriptions with PayPal gating**: Discoin is now a shared multi-tenant bot. Trading economy, gambling,; bank/profile, and basic buddy management stay free everywhere; AI,; fishing, crafting, delves, expeditions, buddy battles/breeding/market, (`d3570bd1`) + +--- + +## [main] -- 2026-05-07 + +### Changes +- **Disc.Fun stakes pay 4x more, ride 2x the volatility**: bumped the staking yield knobs in `Config.DISCFUN` 4x across the board (`staking_emission_dfun_per_day` 5k -> 20k DFUN/day, `staking_max_apy_pct` 10,000% -> 40,000%, `staking_min_apy_pct` 25% -> 100%, legacy `staking_apy` fallback 0.50 -> 2.00) so the live emission APY -- floor and cap included -- comes out 4x higher at every TVL level. Doubled `graduation_daily_vol` from 10% to 20% so the graduated tokens that back fun stakes now move twice as hard intraday: bigger drawdowns, bigger ramps, materially more risk on the principal you're staking. + +## [main] -- 2026-05-06 + +### Bug Fixes +- **Fix `,fun buy SYM all` always erroring with "Insufficient DFUN balance"**: the `all` / `max` / `X%` paths read the player's raw DFUN balance, divided by `SCALE` to get a float, then `_execute_buy` round-tripped that float back into raw via `to_raw`. Float64 can't represent every 18-decimal raw integer exactly, so the reconverted `requested_raw` consistently landed one or two raw units **above** the true holding -- which made `update_wallet_holding` reject the deduction with the misleading "Insufficient DFUN balance" message even when the player had asked to spend everything they had. `_execute_buy` now re-reads the live wallet holding and clamps `requested_raw` to it before charging; a 100% spend always lands at exactly the player's real balance now. The fix also covers the panel quick-buy chips so they can never overshoot for the same reason. +- **Fix `,today` / `,start` home tab missing the Beachcomb / Scavenge quick-action buttons**: the per-game tabs picked up the new buttons last commit, but the **home** tab routes through `services.hub.HubSummary` and that path only knew about `forage_ready`. So the home tab quietly showed a Forage button at the right time but never surfaced a Beachcomb or Scavenge button. `_ready_to_claim_hints` now probes `last_beachcomb_at` / `last_scavenge_at` (scavenge stays hidden during an active dungeon run, matching the in-cog gate), `HubSummary` carries the new `beachcomb_ready` / `scavenge_ready` flags, and the home view's button-injection loop adds the matching success-style buttons whenever the cooldown is up. Same DB-clock pattern (`EXTRACT(EPOCH FROM (NOW() - col))`) as forage so the three free-loot wander commands are now perfectly symmetric across `,today`. +- **Fix `,fish beachcomb` awarding a phantom `cricket` bait that doesn't exist**: `BEACHCOMB_BAIT_POOL` listed `"cricket"` as one of the four pickable bait keys, but the `BAIT` catalog only defines `worm/shrimp/minnow/neon/magic/chum`. So when a `bait_stash` outcome rolled cricket, the count landed in `user_fishing.bait_inventory["cricket"]` where it could never be used for fishing (`bait_meta("cricket")` returned None) and `nft_backfill` spammed `no contract for bait.cricket` warnings on every boot. The pool is now `(worm, shrimp, minnow, neon)` -- still cheap-to-mid as the comment intends, just using a real bait key. Migration `0227` is a one-shot cleanup that folds every existing `cricket` count into the same player's `worm` stack (capped at worm's 500 max_stack so nobody overflows) so legacy stacks aren't lost. +- **Fix listing a brand-new buddy on the AH erroring with "Couldn't find buddy #N's NFT"**: `services.auction.find_owned_buddy_token` validated ownership in `cc_buddies` and then required an existing `item_instances` row, but buddies don't actually have NFTs minted at hatch time -- the legacy `create_listing` path mints them lazily at first list. The buddy panel's "List on AH" button (and `,ah list `) hit the strict path instead, so any never-listed buddy bounced with the cryptic "escrowed / wrong owner" message even when the owner was correct. The lookup now lazy-mints a token when the cc_buddies row exists but no item_instances row does, matching what the legacy path always did. `mint_token` is idempotent on (source_table, source_id) so concurrent calls converge on one row. +- **Fix `,start` panel hiding `delve scavenge` and `fish beachcomb` quick-action buttons**: the farming tab probed `last_forage_at` to surface a Forage button whenever the 10-minute cooldown was satisfied, but the fishing and delve tabs never picked up the same pattern -- so two cheap, free-loot commands stayed buried. Each tab now reads `last_beachcomb_at` / `last_scavenge_at` and exposes a Beachcomb / Scavenge button via the same DB-side cooldown probe (`EXTRACT(EPOCH FROM (NOW() - col))`). Scavenge stays hidden during an active dungeon run (the underlying command refuses mid-floor). +### New Features +- **`,buddy storage` panel ships List-on-AH and Gift buttons**: the storage section now matches the buddy panel's commerce surface so a player doesn't have to leave the panel to put a buddy on the auction house or hand one to a friend. New "List on AH" and "Gift" buttons swap the dropdown to OWNED buddies (since both flows require `status='owned'`); selecting a buddy pops the same price modal the buddy panel uses for AH listings or a recipient-id modal for gifting. Both modals route through the existing `services.auction.create_listing_by_token` / `services.buddy_market.gift_buddy` pipelines so safeguards (gas, escrow, fee, audit row) stay identical. + +### Bug Fixes +- **Fix listing a buddy on the auction house crashing with `CheckViolationError`**: `services/auction.py` sets `cc_buddies.status = 'auction'` when escrowing a buddy, but migration `0220` rebuilt the status check constraint with only `('owned', 'shelter', 'stored', 'nesting')` and omitted `'auction'`. Every AH buddy listing attempt immediately failed at the DB level. Migration `0226` adds `'auction'` back to the constraint so the escrow write succeeds. +- **Fix stale "Pray" / "Open Chest" buttons on `,start` home panel**: after clicking the shrine Pray or dungeon chest Open-Chest quick-action button on the home tab, the panel was not refreshed -- so the button stayed visible and a second click produced a confusing "No shrine here" error. Both buttons now trigger a full home-tab refresh after the underlying command completes, so the button disappears once the one-shot action is consumed. + +- **Fix `,craft specialize` crashing for players who bought the third-slot unlock**: migration `0172` added a DB-level CHECK constraint that hard-capped ``active_specialties`` at 2 entries; migration `0179` then introduced the purchasable ``extra_specialty_slots`` column and the service layer started enforcing ``cap = ACTIVE_SPECIALTY_CAP + extra_specialty_slots``, but the CHECK was never updated. Result: the moment a player with the third-slot unlock tried to specialize their third pick the UPDATE failed with `new row for relation "user_crafting" violates check constraint "user_crafting_active_specialties_chk"`. Migration `0225` replaces the constraint with one that mirrors the service formula (`array_length <= 2 + COALESCE(extra_specialty_slots, 0)`) so the DB and the app always agree. Idempotent. + +- **Fix `,buddy nest collect` / `,buddy nest cancel` crashing with `'Connection' object has no attribute 'fetch_one'`**: the new `_return_parents` helper (which routes nesting parents back to active inventory or storage on collect/cancel) was passing the raw asyncpg ``Connection`` to ``services.buddy_economy.user_max_battle_slots`` / ``user_max_storage_slots``. Those helpers call ``ensure_state(db, ...)`` which calls ``db.fetch_one`` -- a method that lives on the project's PgDatabase wrapper, not on the raw asyncpg connection. Helper now takes the ``db`` wrapper and uses ``db.fetch_one`` / ``db.execute`` / ``db.fetch_val`` throughout. The transaction contextvar already routes wrapper calls through the surrounding ``db.transaction()`` block, so the operation stays atomic. +- **Fix Disc.Fun stake button presses crashing with `'_ButtonCtxShim' object has no attribute 'user_row'`**: the shim used ``__slots__`` to keep memory tight, but the framework middleware (``@ensure_registered`` on the staking commands) writes ``ctx.user_row = await ctx.db.ensure_user(...)`` as a side-channel attribute on whatever ctx-like object it gets. Slots blocked the assignment and any button that re-issued a staking command (My Stakes / Claim All / Stake Everything) crashed before the callback ran. Dropped the ``__slots__`` declaration so middleware can attach ``user_row`` (and any future side-channel field) the same way it does on a real ``DiscoContext``. + +- **Wire the third crafting-specialty slot through every display surface**: `,shop buy specialty_slot` correctly bumps `user_crafting.extra_specialty_slots` and `services.crafting.add_specialty` correctly enforces `cap = ACTIVE_SPECIALTY_CAP + extra_specialty_slots` -- but three display sites still rendered the cap as the bare base value, so a player who bought the third slot saw `Active 3/2` on `,craft specialties` and `,craft book` (which made the extra slot read as broken even when it worked). All three now read `extra_specialty_slots` from the state row and render the effective cap with a `+N purchased` tag when the player has bought any. The recipe-lock error in `services.crafting._do_craft` (when a player tries a `requires_specialty` recipe outside their active set) likewise reflects the live effective cap instead of dangling a stale "or N+1 after buying" hint. Help text in `,help` and the `,craft specialties` docstring also call out the third slot explicitly so the unlock is discoverable from `,help` alone. + +- **Fix `,group pool harvest` burning the entire LP position instead of claiming fees**: the legacy `harvest_group_lp` helper read the position's *total* `lp_shares`, removed all of it from the pool, and credited the proceeds to `reserve_usd`. After harvest the position was at `lp_shares = 0`, which silently disqualified it from every future per-tick LP-yield sweep (`services/lp_yield.tick_lp_yield_for_guild` filters on `lp_shares > 0`). Result: a "harvest" call that the founder thought was claiming fee earnings actually nuked their principal and the LP value stayed pinned at 0 forever. **Fix:** migration `0224` adds `group_lp_positions.cost_basis_usd_raw`, backfilled from each active position's current pool value. New helper `harvest_group_lp_fees_only` computes `gain = current_value_usd - cost_basis_usd`, removes only the fraction `gain / current_value` of LP shares, and leaves the principal in the pool to keep earning. The cog command now previews "Position / Fees Accrued / Receive A / Receive B" instead of "Your LP / share %", refuses with a clean message when the position is at-or-below cost basis ("no fees accrued yet -- wait for trade volume"), and resets `last_yield_at = NOW()` on success so the next yield tick doesn't double-pay over the just-harvested window. The destructive `harvest_group_lp` helper is removed -- nothing else called it. +- **Recovery path: `,group pool deposit ` for founders**: groups whose LP got nuked by the legacy harvest can re-seed straight from their `reserve_usd` without re-running the partnership flow. The command pulls `USD` from reserve, splits it half-and-half across both pool sides at current oracle prices, mints LP at the pool's exact ratio (`min(total_lp * add_a / reserve_a, total_lp * add_b / reserve_b)` so we never over-mint when prices drift), and bumps `cost_basis_usd_raw` by the deposited USD so the new contribution counts as principal in subsequent fees-only harvests. Confirmation embed shows the planned A/B contribution before charging; any failure mid-deposit refunds the reserve. +- **Cost basis tracked at partnership creation too**: `seed_group_pool` now accepts a `cost_basis_usd_per_side` kwarg and writes it on each freshly-minted position. Both `_seed_group_pool_from_vault` and `_seed_group_pool_from_reserve` already had `seed_usd` computed and pass it through, so future partnerships start with an accurate cost basis from minute zero. + +- **Fix `,fun autocompound SYM on` crashing with `column auto_compound does not exist`**: Migration `0218` was authored in two passes -- the early draft created `discfun_stakes` *without* `auto_compound` / `total_compounded`, then the final draft added them via `ADD COLUMN IF NOT EXISTS`. Hosts that ran the early draft already have `0218` recorded as applied in `schema_migrations`, so the column-add follow-up never re-runs. Migration `0222` is a dedicated rescue that idempotently force-adds both columns. Safe on every other environment too -- the `IF NOT EXISTS` clauses no-op where the columns are already present. +- **Disc.Fun graduation now also registers the new token on Moon Network**: the SYMBOL/MOON bridge pool seeded by graduation is, by definition, part of the Moon Network economy, but `services.discfun.graduate_proto_token` only wrote `network_accepted_tokens` for Discoin Network. Result: Moon Network wallets couldn't hold the graduated token and `,trade pools` classified the SYMBOL/MOON pair as Discoin Network because the custom token's home network is dsc. Graduation now calls `add_token_to_network_wallet` for both Discoin and Moon Network. `cogs/trade._pool_network_label` also got smarter: any pool that pairs against a known network coin (`MOON` / `SUN` / `BTC` / `ETH` / `DSC`) is classified by that coin's network first and only falls back to "first token with a configured network" when neither side is a network coin. Migration `0223` backfills Moon Network acceptance for every existing graduated Disc.Fun token. SYMBOL/DFUN and SYMBOL/DSC stay on Discoin Network as expected -- those pairs use Discoin-Network tokens on both sides. + +### New Features +- **`,items` browser is now a 3-tier interactive navigator with auction-house, stack, and junk-sell controls**: replaces the old single-dropdown layout with **Category -> Item Stack -> Item** dropdowns plus a dedicated action-button row. Tier 1 is the existing kind filter (buddy/egg/fish/junk/...). Tier 2 lists every owned **stack** (contract) within the current category, so a player with five trout, three boots and a shiny ring can drill into one stack at a time instead of scanning the whole bag. Tier 3 lists each individual **token** within the chosen stack with its full token id visible -- no more guessing which sibling is which. Picking a token pivots the embed to an inline inspect view and lights up the action row: **List on AH** opens the price/currency modal, **Transfer** opens the recipient modal, **Inspect** sends the full ephemeral inspect card with history, and **Sell Junk** sweeps the player's fishing + dungeon junk inventories at once (paying out LURE + RUNE on separate lines). All four buttons disable when not applicable -- e.g. Transfer is greyed until a token is picked, Sell Junk only enables in the junk category or on a junk stack. Selecting a category resets stack + token, selecting a stack resets token, so the navigation never gets confused about scope. The List/Transfer modals are now standalone (take `ctx + token_id` directly), so the same surface is reusable from the inspect card and the new browser without duplicating the flow. +- **Disc.Fun staking APY is now variable, like AAVE/DSY**: switched from a fixed 50% APY to an emission-based curve (`services.discfun.current_staking_daily_rate`) mirroring `services.safety_module.sm_current_daily_rate`. A configurable daily DFUN pool (`Config.DISCFUN["staking_emission_dfun_per_day"]`, default `5,000 DFUN/day`) is split across every staker in the guild, so daily_rate = emission / total_staked_dfun_value, clamped between `staking_min_apy_pct` (default 25%) and `staking_max_apy_pct` (default 10,000%). Early stakers with low TVL earn near the cap, and the rate compresses as TVL grows but never drops below the floor regardless of how much is staked. `,fun stake` receipts, `,fun stakes`, the new `,fun stake` overview, and `,mystakes` all show the live APY and update when reserves change. The legacy `staking_apy` knob is kept as a fallback when the variable path can't compute (e.g. if emission is configured to 0). +- **`,fun stake` overview (called with no args) lists USD position value, server-wide TVL, live APY, and quick-action buttons**: matches the UX of `,farm stake` / `,fish stake`. The embed shows `live APY`, `cap` and `floor`, `daily emission`, server `TVL` (in DFUN + USD), the user's total staked + pending value (in DFUN + USD), and a "My Stakes / Claim All / Stake Everything" button row that re-issues the matching command on behalf of the presser. The single-symbol stake receipt and the `,fun stakes` list also pick up an action-row of buttons (`Claim`, `Unstake All`, `Toggle Auto-Compound`) so common follow-ups are one click away. Powered by an extended `_ButtonCtxShim` that proxies `reply` / `reply_error` / `reply_success` through the interaction's followup so any cog command can be safely re-invoked from a button. +- **`,fun admin` subgroup for server admins to manage protos**: gated by `Manage Server` permission, never invocable as slash. Subcommands: + - `,fun admin list` -- every proto in the guild with creator, holder count, last-buy age, graduation %. + - `,fun admin rename SYMBOL "New Name"` / `,fun admin emoji SYMBOL ` -- free, bypasses the creator-only check on `,fun edit`. + - `,fun admin destroy SYMBOL [reason]` -- erase a non-graduated proto (DELETE cascades to holdings + trade history). Requires confirmation, refuses on graduated tokens (those are regular guild tokens at that point). + - `,fun admin extend SYMBOL ` -- push the inactivity-sweep timer forward by N days (1-30) so a proto a community wants to keep alive doesn't get auto-destroyed during a quiet week. + - `,fun admin sweep` -- run the inactivity sweep on demand instead of waiting for the hourly tick. Returns the list of destroyed protos. +- **Disc.Fun staked positions count toward net worth and show up in `,mystakes`**: `services.discfun.user_staked_value_dfun(db, gid, uid)` returns `(staked_value_dfun, pending_dfun)` for the user's active stakes, and `services.net_worth.compute_net_worth` adds both to `disc_fun_value` so a graduated proto locked in `,fun stake` keeps showing up on `,balance` / dashboards instead of visually evaporating. `,mystakes` (the unified `,stake mine` view) gains a new `🎢 Disc.Fun` category with per-symbol staked / pending tiles in DFUN + USD, the live variable APY in the title, and the same auto-compound 🔁 / ⚙️ manual badge as `,fun stakes`. + +- **Disc.Fun deploys accept Discord custom emojis**: `services/discfun.validate_emoji` and `cogs/discfun._DEPLOY_RE` now accept either a unicode glyph (1-4 chars, no whitespace -- the previous behaviour) **or** a Discord custom emoji mention `<:name:id>` / ``. The deploy regex bumps the emoji slot from `\S{1,4}` (which silently dropped the longer custom-emoji form) to `\S{1,80}` and lets the validator decide what's allowed. The string is stored verbatim on `proto_tokens.emoji`, and discord.py renders it inline anywhere the bot can see the source emoji (i.e. any guild Disco shares with the player). Animated emojis work too. +- **`,fun edit SYMBOL "New Name" [emoji]` lets the original deployer rename / re-emoji a proto**: New service `edit_proto_token(...)` and command `,fun edit` charge a flat **2x deploy fee** in DFUN (burned), refuse if the caller isn't the original creator, and refuse if the proto has already graduated (post-graduation it's a regular guild token and Disc.Fun no longer owns its metadata). Either field is optional individually but at least one has to change, and a no-op edit is rejected up-front so nobody pays for nothing. Includes a confirmation dialog showing the diff (`name old → new`, `emoji old → new`) before charging. If the proto graduates between the eligibility check and the UPDATE, the fee is automatically refunded. +- **Disc.Fun protos auto-destroy after 7 days of no buys**: Migration `0221` adds `proto_tokens.last_buy_at TIMESTAMPTZ` (default NOW(), backfilled from the latest `proto_token_trades` buy or `created_at` for fresh protos). Every `buy_proto_token` stamps `last_buy_at = NOW()` inside the same UPDATE that bumps the curve, so the timer is atomic with curve state. A new background task in `cogs/discfun.DiscFun.cog_load` runs `sweep_inactive_protos` hourly: any non-graduated proto whose `last_buy_at` is older than **7 days** is DELETEd, and every holder's balance + the audit trail vanish via `ON DELETE CASCADE` on `proto_token_holdings` / `proto_token_trades`. **Sells do NOT refresh the timer** -- only buys count, since a proto kept on life support by sells is dead by definition. The `,fun info` panel now shows a live "Auto-destroy: `Xd Yh` if no buys" countdown so deployers can see exactly how close they are to losing the token. + +### Bug Fixes +- **Fix Disc.Fun chart Y-axis showing 14-digit raw-scale prices**: `proto_token_trades.price_after` is persisted at the project-wide raw 1e18 scale (the column is `NUMERIC(36, 18)` but `buy_proto_token` writes `BuyQuote.spot_price_raw = (virtual_quote * SCALE) // virtual_token` directly, so a real price of `2.7e-5 DFUN/token` lands in the column as `2.7e13`). `services/discfun.build_proto_candles` was reading the column as-is and feeding it straight into the lightweight-charts payload, which is why the chart's Y-axis read `27500000000000.00` and the OHLC header reported `H: 27,054,145,366,503` for HRMZ. The candle builder now divides by `SCALE` on read, matching the units that `synthetic_origin_candle` and `current_spot_candle` already produced via `v_q / v_t`. No migration needed -- the existing rows are mathematically correct, they were just being rendered at the wrong magnitude. Existing trade history snaps back to sane prices the next time anyone runs `,fun chart`. + +- **Buddies in the nest no longer occupy your active inventory**: Previously a player could fill all 10 nest slots with two parents each (20 buddies total), but the active battle cap is only 10, so the math was nonsense -- parents stayed `status='owned'` while incubating and crowded out new captures / hatches. Deposited parents now flip to a new `status='nesting'` state (migration `0220`) that is excluded from BOTH the battle slot cap AND the storage slot cap, freeing up the active slot the moment they go in. On `,buddy nest collect` and `,buddy nest cancel` each parent is routed back into your inventory: first preference is active (status='owned') if there's room under your battle cap, fallback is storage (status='stored') if there's room there, and only if BOTH are full is the action refused with a clear "free a slot first" message (the egg / nest stays put until you do). The deposit, collect, and cancel embeds now also show where each parent landed. Guild-leave / ban paths sweep nesting buddies into the shelter alongside owned ones and clear the orphaned `cc_buddy_daycare` rows so the table doesn't drift. +- **Fix Disc.Fun protos getting stuck mid-curve unable to graduate**: After migration `0219` lowered `graduation_quote` from 50M DFUN to 10M DFUN without retuning the virtual reserves of in-flight protos, a curve tuned for the old 50M target could fill its 800M-token supply cap before the new 10M DFUN target was hit -- leaving the proto un-buyable ("Buy exceeds remaining curve supply") AND un-graduated. `services/discfun.buy_proto_token` now clamps any buy that would overflow the curve cap: the buy is sized down to exactly fill the remaining supply (using a fresh constant-product inversion `_max_quote_in_for_supply_cap`), the unused DFUN is never charged (so the wallet only sees the actual amount used), and the proto graduates on the spot regardless of `real_quote_collected` vs `graduation_quote`. The buy receipt picks up a "Refunded" field showing how much DFUN didn't fit. Defensive fallback: if a stale row somehow has `circulation == curve_supply` and `graduated = FALSE`, the next buy attempt forces graduation so the proto never sits in a permanently-locked state. + +### New Features +- **`,fun help` overview lists the staking and chart commands**: The `,fun` overview embed now ships a dedicated "Staking (graduated tokens)" field listing `fun stake / stake everything / stakes / claim / autocompound / unstake` with a one-line description per command and the live APY pulled from `Config.DISCFUN`. The base command list also adds `,fun chart SYMBOL [tf]` so the candlestick view is discoverable from the overview without having to dig through the `,help` tree. + +- **Disc.Fun proto-token launchpad (Pump.fun in Discord)**: `,fun deploy SYMBOL "Name" 🚀` now lets any registered player launch a proto token on the Discoin Network with a flat 50 DFUN fee - no Protocol Dev tier required. Each proto trades against a virtual-DFUN bonding curve (1B total supply, 800M sold on the curve, 1% trade fee) and graduates automatically once **5,000 real DFUN** has been collected. Graduation seeds two real pools at once -- a deep `SYMBOL/DFUN` pair (90% of the LP slice + all collected DFUN) plus a `SYMBOL/DSC` bridge (remaining 10%) -- and locks the LP forever, so liquidity is permanent. The full command set is: `,fun deploy / list [hot|new|progress|mcap] / info / buy / sell / bag / grads`. The `info` panel ships with quick-buy chips, custom buy/sell modals, top-holders and recent-trades buttons, and a refresh button so anyone in the channel can ape in without leaving the embed. Adds **DFUN** as a built-in Discoin Network token (Disc.Fun -- 🎢) so DSC/DFUN, DFUN/DSD and DFUN/MOON pools auto-seed at boot, on-ramping every major into the launchpad. Trade-off vs `,token deploy`: deployers pick name, symbol and emoji only -- every other knob (supply, virtual liquidity, graduation threshold, post-graduation burn rate and transfer fee, daily volatility) is locked to `Config.DISCFUN`, which keeps the launchpad cheap and tier-free while preserving `,token deploy` as the powerful path with full control over the contract. +- **`,fish trap place all` places every eligible trap at once**: Running `,fish trap place all` (or `,fish trap set all`) deploys all traps you own across every trap type in a single command. Traps too low-tier for your current zone are silently skipped and noted in the confirmation embed. Highest-tier traps are placed first so the best yield traps take priority when the 8-trap cap is tight. +- **Safety Module: variable APY (up to 10,000%) and auto-compounding**: AAVE and DSY staking now uses an emission-based dynamic APY -- a fixed $500/day USD pool is split among all stakers, so early stakers with low TVL can earn near 10,000% APY while the rate compresses naturally as more is staked (Cetus-style). A new `,stake aave autocompound` / `,stake dsy autocompound` toggle lets players flip between two modes: ON re-stakes accrued yield back into the position as more AAVE/DSY every hour via the staking tick; OFF pays yield in USDC/DSD to the DeFi wallet on manual `,claim` as before. The status embed shows the live current APY, pending compound or yield, and the auto-compound toggle state. +- **Multi-slot buddy nest with shop upgrade**: Players start with one nest slot and can buy up to nine more from `,buddy shop` (`,buddy slot nest buy`, 10,000 BUD per upgrade, max 10 simultaneous). `,buddy nest` now lists every active slot with its own timer, `,buddy nest collect [slot]` and `,buddy nest cancel [slot]` accept an optional slot id, and the `cc_buddy_daycare` table is keyed on a serial id post-migration `0215` so multiple parallel deposits coexist. Lets a serious breeder run several pairs at once instead of waiting six hours per egg. + +- **`,fun chart SYMBOL [tf]` renders a Disc.Fun-themed candlestick chart**: New command pulls the proto's bonding-curve trade history from `proto_token_trades`, aggregates into 1m / 5m / 15m / 1h / 4h / 1d candles (default `5m`), and renders through the existing playwright pipeline using a dedicated `charts/template_discfun.html` template. Coloration is intentionally distinct from `,chart` so a viewer never confuses a pre-graduation curve with a graduated AMM token: deep-indigo background (`#1a0d2e`), magenta/violet grid, **cyan/hot-pink candles** (vs the green/red of regular charts), a hot-pink pair label with a "DISC.FUN · BONDING CURVE" badge, and a footer that reads "Disc.Fun • Pre-graduation virtual liquidity". The chart also draws a dashed yellow **graduation-price line** at the spot the curve will reach when it graduates, so traders can see how much room is left. The `,fun info` panel adds a `📊 Chart` button alongside Buy/Sell/Refresh that opens the chart inline. Genesis-pad + trailing live-spot candle keep the chart usable on a brand-new launch with only a trade or two; falls back gracefully when playwright isn't installed. +- **`,fun buy` accepts `all` and `X%` now too**: `,fun buy HRMZ all` previously errored with "amount must be positive" because the buy parser only handled raw numbers (`100`, `1k`). It now matches the `,fun sell` and `,fun stake` UX -- `all` / `max` spends the player's entire DSC-network DFUN balance, `25%` spends a quarter of it, and existing `100` / `1k` / `2.5m` numeric forms keep working. Empty-balance call gives a clean "buy some first" hint instead of a confusing math error. +- **Disc.Fun graduation threshold lowered from 50M to 10M DFUN, and existing protos migrated**: 50M was a stretch goal that very few launches would realistically reach; 10M lands in pump.fun's heavier-volume territory while still feeling earned. Virtual reserves retuned to keep the same ~121x price ramp (start ~1.14e-3 DFUN/token, final ~0.138 DFUN/token, ~$13.8M FDV at graduation anchored at $0.10/DFUN). Deploy fee scaled proportionally to 10,000 DFUN and quick-buy chips to `100 / 500 / 2,000 / 10,000 / 50,000 DFUN`. Migration `0219` rewrites `graduation_quote` from 50M to 10M on every existing un-graduated proto in place -- collected real-DFUN progress carries over (a curve already at 6M is now 60% of the way to graduation, not 12%), and the live virtual reserves are left alone so mid-flight curves stay continuous. Idempotent: only rewrites rows still at the legacy 50M. +- **`,fun deploy` is now rate-limited to one launch per user per server per 24 hours**: Prevents one player from spamming a dozen meme protos in a single afternoon and crowding the launchpad. The check uses the most recent `proto_tokens.created_at` row for `(guild_id, creator_id)` and a DB-side `EXTRACT(EPOCH FROM (NOW() - created_at))` window, so a failed or cancelled deploy doesn't burn the day's slot (no row gets inserted in those paths). Players see the remaining hours/minutes before the confirm dialog, and the service layer rejects the same window atomically as the source of truth. Help text on `,fun deploy` (no args) now lists the limit alongside the deploy fee. +- **Disc.Fun staking adds an autocompound toggle**: `,fun stake SYM autocompound` (or the alias `,fun autocompound SYM [on|off|toggle]`) flips the position into virtual-compound mode -- accrued DFUN yield is converted at the live SYMBOL/DFUN spot price into more of the staked token and added back to `amount` on every accrual, instead of accumulating as `pending_dfun`. The conversion is virtual (no AMM round-trip), so frequent compounds don't churn the pool reserves and the staker gets clean compounding without slippage on the compound step. Toggle accrues under the OLD mode first so any DFUN already pending stays claimable when flipping ON, and any compounded SYM stays put when flipping OFF. `,fun stakes` now shows a 🔁 **AUTO** / ⚙️ manual badge per row plus a "lifetime auto-restaked" stat, and the stake-receipt footer hints at the toggle command. +- **Disc.Fun staking: graduated tokens earn DFUN yield**: Anyone holding a graduated proto can now `,fun stake SYM ` (or `,fun stake everything` to stake the full balance of every graduated Disc.Fun token they own at once) and earn DFUN at a flat **50% APY**. Yield is denominated in DFUN, proportional to the position's live spot value via the SYMBOL/DFUN pool reserves (so a token whose price 10x's after graduation immediately starts paying ~10x more yield to its stakers). Companion commands: `,fun stakes` shows live positions with pending yield + lifetime claimed, `,fun claim [SYM]` sweeps accrued DFUN without unstaking (no symbol = claim all), `,fun unstake SYM ` withdraws and auto-claims. Lazy accrual on the DB clock (`EXTRACT(EPOCH FROM (NOW() - last_accrue))`) so positions keep earning even when the bot is offline. New `discfun_stakes` table + migration `0218`. +- **Disc.Fun graduation threshold set to 50,000,000 DFUN flat**: The original 5,000 DFUN target made graduation feel inconsequential. Threshold is now a clean 50M DFUN -- denominated in the launchpad's native quote so the milestone stays stable regardless of the DFUN oracle. Virtual reserves retuned to keep a ~121x price ramp (start ~5.68e-3 DFUN/token, final ~0.69 DFUN/token, end-state market cap ~688M DFUN). Deploy fee bumped to 50,000 DFUN and quick-buy chips to `500 / 2,500 / 10,000 / 50,000 / 250,000 DFUN` so trades land meaningful slippage on the bigger curve. Sanity-checked: 1,011 buys of 50k DFUN graduate at 50,044,500 DFUN collected with 800,064,675 tokens sold (target 800M). +- **`,balance` summary and dropdown now include a `🎢 Disc.Fun` row + tab**: The Summary card's holdings list adds Disc.Fun next to the other systems (sorted by USD value like the rest), and the dropdown gains a dedicated `🎢 Disc.Fun` page that lists every active proto position the player holds with current spot, DFUN value, and a USD total at the top. Powered by the new `NetWorthResult.disc_fun_value` so dashboards/API surfaces inherit the same value automatically. +- **`,fun bag` and trade receipts show USD next to DFUN, and active proto positions count toward net worth**: Bag totals now print Value / Cost / PnL on three lines, each with a live USD equivalent (computed from the current `crypto_prices` row for DFUN, falling back to the genesis $0.10). Buy / Sell receipts also append `($X.XX)` to the Paid / Received / Fee fields and the New Price / Avg Fill rows so it's obvious what a trade is worth without doing the conversion in your head. Net worth now counts active Disc.Fun positions via a new `NetWorthResult.disc_fun_value` field; `services/discfun.user_active_value_quote()` is the single source of truth (still used by `,fun bag` for display) and `services/net_worth.compute_net_worth()` translates it through the live DFUN oracle so `,balance` summary and the dashboard pick it up automatically. + +### Bug Fixes +- **Fix bot failing to start after migration `0219` with `bigint out of range`**: Postgres was parsing `10000000 * 1000000000000000000` (10M * 1e18 = 1e25) as `int8 * int8`, which overflows `int8` (max 9.2e18) before the result ever reaches the NUMERIC(36,0) column. Both operands now get an explicit `::NUMERIC` cast so the multiplication happens in unbounded precision: `10000000::NUMERIC * 1000000000000000000::NUMERIC`. The migration is otherwise unchanged (and still idempotent: only rewrites rows still at the legacy 50M target). +- **Fix `,fun list` showing prices in scientific notation**: A fresh proto's price ``1.4423e-06 DFUN`` was rendered straight from Python's default ``%g`` format, which is illegible for memecoin-tier prices. The formatter now uses pump.fun-style subscript-zero notation (``0.0₅1442 DFUN``) for any value below ``1e-4`` and switches to thousands-separated decimals above that. Market caps and prices everywhere (``,fun list``, ``,fun info``, ``,fun bag``, deploy / buy / sell receipts) now also append a USD equivalent computed from the live ``crypto_prices`` row for ``DFUN`` (falling back to the genesis price), so a fresh ``HRMZ`` shows ``💰 0.0₅1442 DFUN ≈ $0.000000144 · 📊 mcap 1.44k DFUN ($144.23)`` instead of an unreadable ``1.4423e-06``. +- **Fix `,fun deploy` (and every other `,fun` button) crashing with "property 'parent' of '_QuickBuyButton' object has no setter"**: The Disc.Fun panel's quick-buy / buy / sell / refresh / holders / trades buttons all stored a back-reference to their `FunPanelView` as `self.parent`, but recent `discord.py` builds make `discord.ui.Item.parent` a read-only property (same family of bug as the earlier `_DbBrowseView._snapshot` regression). Renamed the attribute to `self._panel` across all six button classes so the panel constructor no longer triggers `AttributeError` on every `,fun deploy` / `,fun info`. +- **Fix Safety Module APY bottoming out near 0%**: The emission pool was $500/day, which at realistic TVL levels produced sub-1% APY. Raised `emission_usd_per_day` to $50,000/day so the curve stays generous at higher TVL (e.g. ~80% at ~$22M TVL vs. 0.8% before). Added a hard `min_apy_pct` floor of 50% so APY can never drop below 50% regardless of how much is staked. The full live range is now 50% -- 10,000%. +- **Fix `,delve flee` misleadingly suggesting it exits the dungeon**: `,delve flee` only escapes the current mob fight -- the player is still inside the dungeon and must use `,delve rest` afterward to return to the surface. The scavenge error message used to say "flee your current run" which made players think flee was a full exit; it now says "use `,delve rest` to exit (`,delve flee` first if in combat, then `,delve rest` to leave)." Both flee success embeds (command and combat button) now carry a footer: "Still inside the dungeon -- `,delve rest` to exit and restore HP." +- **Fix `,db` kind dropdown crashing with "value must be 1-100 chars"**: `_DbKindSelect` used `value=""` for the "All kinds" option, which Discord rejects (minimum 1 character). Changed to `value="_all_"` and mapped it back to an empty-string kind in the callback so the rest of the browse logic is unchanged. +- **Fix `,db browse` crashing with "property '_snapshot' has no setter"**: `_DbBrowseView` used `self._snapshot` for its back-button breadcrumb, but `discord.ui.View` reserves that name as a read-only property in recent discord.py builds, causing an `AttributeError` on every browse open. Renamed the attribute to `_browse_snap` throughout the class so it no longer conflicts. +- **Combine `,buddy stake everything` into one confirmation embed**: Staking all FREN+BBT sent a separate receipt embed for each token, flooding the channel with two messages. The "everything" branch now builds a single embed showing both tokens in one description block (FREN section + BBT section), so players see one clean confirmation instead of two. + +### Changes +- **Auto-pump fires silently (no channel announcement)**: The per-guild market-event announcement that the auto-pump scheduler used to post to `events_channel` / `crypto_channel` has been removed. Pumps still happen and are visible in price charts and trade output; they just no longer broadcast a spoiler embed. Admins can still see what fired via `,admin pump active`. +- **Auto-pump interval randomized per-guild between 1 and 60 minutes**: Previously the window was ~55-75 minutes. Now each guild independently rolls a new wait between 60 s and 3600 s (1-60 min) after every fire, making the market feel more alive and unpredictable. +- **Delve tokens (COPPER/SILVER/GOLD/RUNE) pumped together as a tethered group**: When the auto-pump scheduler selects any delve token, all four eligible delve tokens receive the same pattern, magnitude, and duration simultaneously. This keeps their relative price ordering (COPPER < SILVER < GOLD < RUNE) intact across pump events. The four tokens still count as a single slot in the pick pool so they do not crowd out other tokens. +- **`,db` now opens the interactive catalog browser by default**: Calling `,db` with no arguments used to send a static overview embed (kind counts only). It now launches the same `_DbBrowseView` as `,db browse`, giving immediate access to the kind dropdown and item-inspect picker without having to type a subcommand. +- **Pivot between `,buddy storage` and the egg picker on a single message**: The `Eggs` button on `,buddy storage` used to bounce the player out to a stub message asking them to run `,fish egg` (which itself just redirected to `,buddy egg`). It now swaps the same message in place to the egg picker view. The egg picker mirrors the storage panel's row-1 layout (`Withdraw / Deposit / Buddies / Refresh` instead of `Withdraw / Deposit / Eggs / Refresh`), with the new `Buddies` button pivoting back, and the existing `Hatch / Sell / Gift / List on AH` actions move to row 2. `Withdraw` and `Deposit` open a quantity modal so you can shuffle between held + banked egg storage without leaving the panel. +- **Hide delve / buddy / fishing / crafting action receipts so they stop spamming the channel**: Pickaxe Strike, Pray, escaped-buddy battle/result, AH listing, crab-haul, trap-set, egg-hatch, fishing tackle panel, and craft success receipts were all sending as public followups, so a single delve session would post dozens of public messages alongside the live panel everyone was already looking at. All of these are now `ephemeral=True`, matching the existing chest-cracked / kill-drop / potion behaviour -- only the player who pressed the button sees their receipt, the public panel still updates in place for everyone. + +### Documentation +- **Document new buddy nest slot upgrades in `,help shop`**: The Buddy Shop section in `,help shop` now lists the Nest Slot upgrade alongside the existing Buddy Slots and Battle Attractor entries so players discover it without reading the changelog. +- **List ,fish beachcomb and ,delve scavenge in their help pages**: The two free-roll commands shipped earlier but were missing from `,fish help` and `,delve help`, so players couldn't discover them without reading the changelog. Both help pages now show the command, the kinds of loot it can drop, and the cooldown. + +### Bug Fixes +- **Fix `,db browse` crashing with "Embed size exceeds maximum size of 6000" on dense kinds**: `cogs/lexicon.py` packed every catalog row into one embed (up to 200 contracts, ~100 chars each), which routinely tipped over Discord's 6000-char total-embed cap on the Crafted kind. Browse now uses an interactive paginator (12 items per page, single-block description so total embed stays well under 6000) and adds two dropdowns: a row-0 kind picker that switches the catalog without retyping the command, and a row-1 item picker that drills into a contract's full detail entry on the same message. Prev / Next / Refresh / Back buttons round it out so the player can hop between detail and list without losing their place. +- **Hide buddy egg rarity until it hatches**: `,buddy nest` and the deposit success message used to spoil the egg's rarity at deposit time, removing the surprise from the breeding loop. Rarity is still pre-rolled and stored on the row, but the surface only reveals it once the egg is ready to collect (or after it hatches into a buddy). Incubating slots now show the species + countdown only. +- **Potion bag recognises every healing potion in the catalog**: The mid-battle `Potion` button hardcoded the order `elixir > potion_major > potion_minor`, so newer healing potions like Supreme Potion sat unusable in the bag even though they showed up in `,delve inv`. The button now scans every `kind == "heal"` consumable in `dungeon_config.CONSUMABLES` and picks the highest-value one the player owns, so any future heal-kind potion is auto-eligible. +- **Show buddy gender in the shelter listing**: `,buddy shelter` rendered each row without the `M`/`F` glyph, so a player browsing for a breeding partner couldn't tell at a glance which adoptable buddies were male vs female. The query now selects `gender` and the row prints the canonical `M` / `F` symbol next to the buddy name. +- **Surface silent delve buffs in the combat log so spells, potions, and shrine boons are actually felt**: `services/dungeon.py::resolve_attack` was applying `wildshape`, `shrine_atk`, `shrine_spd`, `sanctuary`, `thorn_aura`, and `regen` silently -- the multipliers folded into ATK/SPD/incoming-damage with no log line, so a praying druid mid-fight just saw "you hit for X" with no idea where the boost came from. Each round now opens with a `Buffs: Wildshape +50% ATK, Shrine ATK +20%, Sanctuary halves incoming, ...` banner listing every active per-swing modifier with its actual percentage. Sanctuary additionally logs a `+ Sanctuary soaks **N** damage` line per mob swing so the halving is visible, not implied. `marked_target` swings now show `*(marked, N left)*` next to the CRIT tag so the player can plan whether to burn a heavy ability before the marks fade. `shrine_debt` (the curse outcome that doubles the next chest) is now reported via a new `ChestResult.shrine_debt_mult` field, and both chest-receipt paths (button + `,delve open`) print `🙏 Shrine debt paid off! Rune payout x2.` when it triggers -- previously the 2x kicker was completely invisible. +- **Fix discoin.cc tabs rendering as a thin unusable column on Leaderboard / Predictions / Admin / NFTs / Portfolio / Mining / Lending / Profile / Security Logs**: `components/ui/tabs.tsx` keyed its layout on `data-horizontal:` / `data-vertical:` Tailwind selectors, but Base UI's tabs primitive sets `data-orientation="horizontal"|"vertical"` -- so none of the orientation styles fired. The Tabs root stayed `flex-row` and the list rendered as a thin column on the left of the panel, with most labels squeezed into 1-2 character columns on narrow viewports. Rewrote the component to use `data-[orientation=...]` selectors that actually match, target the Root via the `tabs` group so List styles work even if Base UI doesn't propagate the attribute, redesign the segmented control as a wrapping pill row that always uses full container width, and pass `data-orientation` explicitly belt-and-suspenders so the styles fire regardless of future Base UI changes. +- **Fix `,dev status` crashing with "'CardBuilder' object has no attribute 'to_dict'"**: `_build_module_health_embed` was declared `-> discord.Embed | None` but actually returned the bare `CardBuilder` from `card(...)` without calling `.build()`. Both `,dev status` and the auto-status DM appended the builder to their embed list, which crashed inside discord.py the moment it tried to serialise it. Helper now ends its chain with `.build()` so the caller gets a real `discord.Embed`. +- **Fix `,dev status` UnboundLocalError on `Severity`**: `cogs/dev.py` already imports `Severity` at module level from `core.framework.error_tracker`. The doctor-scan section did `from cogs.health import _SEV_ICON, Severity` inside the function, which Python's compiler treats as making `Severity` a function-local everywhere in `dev_status`/`auto_status_dm` -- shadowing the module-level binding and triggering UnboundLocalError. Renamed the import to `Severity as DoctorSeverity` so the two enums no longer collide. +- **Fix economy security alerts printing $1.01 sextillion totals**: `cogs/economy_security.py` summed `transactions.amount_in/_out` raw, but those columns are `NUMERIC(36,0)` scaled by 10**18; the resulting alerts said things like "INCOME_VELOCITY: 34 LP_YIELD transactions, $1,015,941,903,741,947,871,232.00 earned" which looked like an exploit but was just the wrong unit. All three velocity detectors (income / gambling / transfer) now descale via `to_human()` (`_h`) before summing so the alert text reflects real dollars. +- **Fix auto-status DM falsely reporting Crafting/Farming/Fishing as disabled**: `_check_modules` used `settings.get(col, True)` to read each `module_*` flag, which returns the actual value when the column exists -- and `module_crafting/farming/fishing` are stored as `BOOLEAN NULL` (NULL meaning "enabled by default" per their migrations). `dict.get` treated None as "disabled". Now uses `settings.get(col) is not False`, matching the canonical `,admin ` truth table. +- **Fix module health rollup pointing at non-existent tables**: `_MODULE_CATALOG` referenced `farm_plots` and `fishing_inventory` which don't exist; the real tables are `farming_harvests` and `fishing_catches`. Same fix for `expeditions` -> `buddy_expeditions`, `quests` -> `user_quests`, `auctions` -> `auction_listings`, `predictions` -> `prediction_markets`, `buddies` -> `cc_buddies`. Healthy systems were showing as silent failures because the count query hit a missing relation. +- **Fix Dungeon row showing 'cog not loaded' on healthy servers**: The catalog used cog class name (`Dungeon`) but `bot.cogs.keys()` exposes the registered name (`Delve` for Dungeon, declared via `class Dungeon(commands.Cog, name="Delve"):`). Catalog now uses the registered name. +- **Fix substring cog-loaded check false-negativing similar names**: The previous `cog_key.lower() in c` substring match could mistakenly identify "ChatLevelingAdmin" as the "ChatLeveling" cog (or miss casing differences). Now uses an exact match against the registered cog name set. +- **Fix ,status quintillion-dollar Pool TVL and Total Staked**: `cogs/status.py` summed `pool.reserve_a/_b` and `pos_validators.stake_amount` raw, but those columns are NUMERIC(36,0) scaled by 10**18; the embed printed totals in the quintillions on every server. Both call sites now descale via `_h(...)` (core.framework.scale.to_human) before summing. +- **Fix ,status Price Engine permanently red**: `_SERVICES` referenced the legacy `price_drift` heartbeat key, which no task pulses anymore (the live key is `price_drift_trade`). The light was red on every server regardless of price-engine health. Same fix applied to `.dev check prices`. +- **Fix `.dev` auto-status DM footer hardcoding the wrong prefix**: The next-report footer hardcoded `.dev config interval`, sending users with any other prefix to a non-existent command. Now composes from `Config.PREFIX`. + +### Changes +- **Rework ,dev status into a next-gen multi-page report**: Adds a 12-segment health-score bar to the page-1 header (matches `,admin health heal` rendering) plus two new pages: a Game Systems rollup (the same per-module catalog the auto-DM uses) and a Doctor Snapshot that surfaces the read-only output of `,admin health heal` -- categorised issue list with severity icons and a 🔧 marker on entries with an auto-fix available. Pre-runs the doctor scan once and reuses the result so the score appears in the header instead of being buried four pages deep. +- **Wire ,admin health heal logic into the auto-status DM**: New module-level helper `cogs/health.py:doctor_quick_scan` exposes the read-only portion of the heal flow (channels / webhook / Redis / task loops / self-heal scheduler / DiagBlocks / bot perms) so the auto-DM and `,dev status` show the same actionable issues a user would trigger a heal for. The auto-DM now sends a Doctor Snapshot embed every interval with a categorised issue list and a one-line auto-fix hint. +- **Rework auto-status DM with module health roll-up**: The 4-hour auto-DM was last touched before crafting/dungeon/expeditions/farming/fishing/quests/achievements/buddies/NFTs/auctions/predictions/chat-XP shipped. Adds a per-module health page that shows cog-loaded vs admin-disabled vs enabled state, per-table row counts, recent error counts, and (where applicable) the matching task-loop pulse age. Distinguishes "admin-disabled" (⚪) from "cog not loaded" (🔴) so an intentionally-off module never shows red. Also adds an economy-sanity tripwire that flags wallet/bank totals above $1e15 as a probable 10**18 leak before the embed reaches the developer. +- **Rework ,status System Health page to interval-aware liveness**: Each row now uses the registered loop interval to decide green/yellow/red instead of a hardcoded threshold, so a 30-min interest loop isn't flagged at 5 minutes and a 15-second drift tick isn't ignored at 4 minutes. Also adds rows for previously unmonitored loops: `lp_yield`, `hourly_summary`, `keeper_loop`, `rugpull_integrity`, `lunar_tick`, `season_expiry`, `challenge_expiry`, `backup`. +- **Extend startup self-test with heartbeat and module-flag coverage**: `_startup_selftest` now also reports task loops registered but never pulsed, and warns when `guild_settings.module_*` columns exist that the modules diag block doesn't know about. Catches drift between schema migrations and the diag list at boot instead of in production. +- **Extend modules diag block with modern module flags**: Adds `module_crafting`, `module_farming`, `module_fishing`, `module_rugpull`, `module_events`, `module_faucet`, `module_security`, `module_ape` to the diag check; previously these were skipped entirely. + +### New Features +- **Add random surprise events to ,work and ,daily**: Both commands now have a small chance (~2.5% on work, 3% on daily) of rolling a positive surprise on top of the normal payout: a Treasure Chest (flat 2x-5x bonus on top of base earnings, $50 floor), a JACKPOT (3x payout multiplier), or a free Validator Guard / Yield Guard dropped into your inventory. Surprises apply after work tax/cap and after all daily bonuses, so they always feel like a clean windfall, never a downside. Each kind has its own flavor text and shows up as a dedicated field in the result embed. +- **Lock status-report invariants in tests/test_status_report.py**: Regression suite (now 18 tests) asserts (a) no status surface references a heartbeat key nothing pulses, (b) the legacy `price_drift` key is gone everywhere, (c) `reserve_a/_b` and `stake_amount` are descaled before display in every status path, (d) the auto-DM footer composes its prefix from config, (e) the modules diag list and per-module health roll-up cover the modern systems, (f) the diag uses the `is not False` pattern so NULL flags are treated as enabled, (g) every cog name in the catalog matches a real registered cog (taking `name="..."` overrides into account), (h) every table referenced exists in `schema.sql` or a migration, (i) `doctor_quick_scan` is exposed and called by both `,dev status` and the auto-DM, (j) both surfaces render the 12-segment health bar. + +### Discord Bot +- **Add in-fight buddy capture, USD-priced stake panels, work breakdown button**: Capture during fishing wild fights: row-1 Capture button mirrors the; delve pattern (gated on enemy HP < CAPTURE_HP_THRESHOLD, label shows; live %). On success: cc_buddies insert + finalise as captured win; (`1a34a21e`) +- **Extend ,ai recontext with server + channel scopes**: The user-only wipe wasn't enough -- guild-scoped facts, the recent; server-events drama feed, and the channel-context (reactions / edits /; deletes / banter) feed all keep feeding hallucinations back to the (`2bca0400`) +- **Add ,ai recontext: full per-user AI state wipe**: ,ai forget only clears the one-line memory summary, but Disco also; reads from conversation history (last 14 turns), inferred traits,; reaction-category counters, long-term facts, and the Redis short-term (`1adb873d`) +- **Fix NameError on SUN mining tick: pass mining_chain="SUN"**: _process_sun_guild was calling _mint_group_vault_tokens with; mining_chain=symbol, but symbol is a parameter of the parent; _process_pow_guild and was never passed through. Group vault minting (`85d154de`) +- **Rework AI context: per-channel/server/user awareness + emoji repair**: Single composer in services/ai_context.py replaces four ~150-line copies of; the system-prompt assembly across ask/reply/mention/ambient handlers. Adds; channel-topic awareness, name/topic-derived persona tags (lounge / market / (`9569c5d8`) +- **Add delve rarity, mini-bosses, ability picker, and protected sell-all**: Five-phase delve overhaul:; 1. Rarity foundation -- every weapon/armor/junk now carries a; rarity (common..legendary). Existing items get a flat 0.80x (`82b4ee30`) +- **Fix Safety Module yield showing 0 -- remove auto-tick, accrue until claim**: The 30-min sm_yield_tick was resetting last_yield on every loop while; paying micro-credits straight into wallets, so ,stake aave status always; showed Pending Yield ~ 0 and players felt nothing was earning. Worse, (`35e32bad`) +- **Fix pump patterns that didn't move the chart, persist events across restarts**: Five pump patterns (chaos, volatile, spike, bull, bear) and the auto-pump; scheduler were firing but the chart wasn't reflecting them:; - chaos: unbounded random walk x 2.2 could push the multiplier negative (`30b87341`) +- **Add ,delve stake all / unstake all / sell all bulk helpers**: Players running ,delve mine in volume had to issue three stake commands; (copper / silver / gold) per cashout cycle and one ,delve sell per; upgrade, which got tedious once you'd cleared a few floors. (`bb5d8434`) +- **Simplify .group reserve embed and help text**: The reserve display previously fanned out three "USD Bucket / BTC; Bucket / Token Vault" inline fields and used "Mining Cut" / "Active; Inflows" -- jargon that hid what the reserve is actually for. The (`3e86e3fd`) +- **Stop ,buddy species hitting Discord's char-max limits**: Two failure modes in the species roster panel:; 1. Per-tier field chunker capped chunks at 1000 chars but wrote them to; 1024-cap fields without a final guard. Combined with longer ability (`dbc49ee7`) +- **Single source of truth for spendable group reserve total**: The reserve number at the bottom of ,group upgrade list and the; "Reserve" field in ,group hall info both excluded the group token; vault, so members saw a smaller number than ,group reserve reported. (`82aef73d`) +- **Unify AAVE/DSY staking under ,stake**: The standalone ,aave and ,dsy command groups have been folded into ,stake; as parallel subgroups so every yield-bearing deposit (farms, validators,; Safety Module) lives under one command tree. (`1670ad12`) +- **Spend group token vault on Hall upgrades**: The reserve embed totalled USD + BTC + token vault, but ,group upgrade buy; only ever counted USD + BTC, so groups whose value lived in the vault; could not spend it on upgrades. Match the spendable total to what's (`88bc54db`) +- **Refresh group system: scaling Hall upgrades + tribute reserve inflows**: - Hall upgrades: 8 -> 22 across 5 lines. New Industry line (Angler's Dock,; Greenhouse Wing, Delve Bastion, Forge Workshop, Guild Market, Master; Industries) grants group-wide bonuses on fishing / farming / delve / (`56284209`) + +### Services +- **Fix ,fish sell crash: EARN_ONLY_TOKENS is a frozenset, not a dict**: Three call sites in services/fishing.py (sell_rod, sell_trap,; sell_all_traps) all had:; reel_network = _Cfg.EARN_ONLY_TOKENS.get("REEL", {}).get("network", "dsc") (`f3b95362`) +- **Block storing a buddy that's on an expedition**: to_storage previously checked for-sale status, the at-least-one-owned; floor, and the storage cap, but never the buddy's expedition status.; Players could deposit an actively-expeditioning buddy and effectively (`eceb219f`) + +### Configuration +- **Bump AAVE/DSY Safety Module yield to ~50% APY (LM 5x to ~5%)**: Config.SAFETY_MODULE.AAVE.daily_yield and DSY.daily_yield are now; 0.001370 (was 0.000548), so stakers earn ~50% APY in USDC / DSD on; the USD value of their stake. lm_daily is bumped to 0.000137 (was (`cdd46f9a`) +- **Bump Safety Module stake yield to ~20% APY and LP ambient APR to 60%**: AAVE/DSY daily_yield 0.000137 -> 0.000548 (5% -> 20% APY in stables).; LP_YIELD_APR 0.30 -> 0.60 so hourly LP rewards double across all; positions; lock/user-token/group/bootstrap multipliers still stack. (`9129fbfc`) + +### Framework +- **Fix backticked custom emoji markup rendering as code, not as image**: The emoji-context system prompt told the model to "paste the raw; <:name:id> markup EXACTLY as shown" with backticks around the example.; The model reproduced the backticks verbatim, producing replies like (`bf87f257`) +- **Accept numeric emojis as amount input**: Translate 💯 -> 100, 🔟 -> 10, and keycap digits 0️⃣-9️⃣ to their digit; form before parsing. Recognises sequences like ,dice 💯 and ,dice 1️⃣0️⃣0️⃣; across every command that uses core.framework.utils.parse_amount or (`84b7f096`) + +### Changes +- **Fill Delve weapon/armor tier gaps for non-Warrior classes**: Rogue's shortsword line jumped from tier 1 (+3 ATK) to tier 7 (+95 ATK); with nothing in between, so once depth-scale push hit floor 35-40 a; Rogue couldn't out-gear the curve and got one-shot. Other class lines (`17861ab4`) +- **Expand crafting: 20 recipes, 8 buddy gear pieces, Forge-Sealed FORGE sink**: Crafting expansion across the existing recipe schema -- pure data, no; service / cog changes required. Routes through existing apply targets; (bait/*, fert/*, consum/*, buddy/equip//) so the receiving (`5962d883`) + +### New Features +- **Starter gear shop with 3 tiers of basic DSD-priced gear**: Adds 12 starter gear items across three tiers (Apprentice/Initiate/; Adept at $1k/$5k/$25k) plus matching cosmetic accessories. Stat; magnitudes are deliberately weaker than the crafted gear (Battle (`734deafb`) +- **5 new species, level-gated abilities, healer rebalance, gear expansion**: New species (Tortuga, Jolt, Phantom, Verdant, Mimik) ship with unique; abilities (Fortress Shell, Static Shock, Phase Shift, Photo Synth,; Ambush) and full ASCII frame sets. Every species also gets two (`6349dd18`) +- **Expand fish facts pool to 5 per species**: Add FISH_FACTS_EXTRA with 2 additional facts per species (~85 fish + crabs).; fish_fact() now merges both dicts before random.choice(), so each species; has 5 rotating facts instead of 3. Distinct angles: ecology, culture, (`279b2ed6`) +- **Add per-species fish facts to catch embed**: - Add FISH_FACTS dict with 3 rotating facts per species (~85 fish + crabs); - Add fish_fact() helper that picks one at random each catch; - Wire into _result_embed: a random fact appears in italics under the (`dd180ee7`) +- **Expand fish catalog with 20 more fish and 8 new crabs**: Fish additions (filling sparse zones):; Sewer -- Sludge Feeder (common), Pipe Goby (uncommon); Tidal Pool -- Blenny (common), Pipefish (uncommon), Goby (common) (`4011e83f`) +- **Add 6 new zones and 17 new fish species**: New zones (24 total, up from 18):; - Tidal Pool (tier 1): near junk-free, clear shallow water; - Mangrove Thicket (tier 2): extra debris but high rare bonus (`d50197b5`) + +### Maintenance +- **Bump axios**: Bumps the npm_and_yarn group with 1 update in the /frontend directory: [axios](https://github.com/axios/axios).; Updates `axios` from 1.15.0 to 1.15.2; - [Release notes](https://github.com/axios/axios/releases) (`118b4e50`) + +--- + +## [main] -- 2026-05-06 + +### Bug Fixes +- **`anglers_paste` / `expedition_ration` and every other crafted-as-consumable item now backfill + lazy-mint correctly**: Crafting recipes whose output `apply: consum/` route into `user_dungeon.consumables` -- but the contracts are deployed under `kind="crafted"`, not `consumable`. The backfill loop only looked under `consumable.` and gave up when `consumable.anglers_paste` didn't exist (warning every time, mint count = 0). New `fallback_kinds` parameter on `_backfill_count_jsonb` resolves the catalog_key against an ordered list of contract kinds; the consumables plan entry now passes `("crafted",)` as fallback. Same kind-bridging extends to `crafted` outputs that route to `user_fishing.bait_inventory` (`apply: bait/*`) and `user_farming.fertilizer_inventory` (`apply: fert/*`) -- backfill plan adds a "fertilizers" entry, and `_LAZY_INV_PLAN["crafted"]` walks all four columns so `,ah list anglers_paste 100` lazy-mints from wherever the player is currently storing it. Also fixed a separate latent bug while in there: the original loop did `break` on the first unknown contract, which silently dropped every subsequent valid key in the same user's inventory; replaced with `continue` so one bad key no longer poisons the rest. +- **Bot was crashing on startup with `CommandRegistrationError: salvage`**: My new `,delve scavenge` command aliased `salvage` but `,delve junk` (line 6195) already owned that alias with `aliases=["salvage", "scraps", "trash"]`. Discord.py rejects duplicate aliases at extension load, so the dungeon cog failed to register and the whole bot setup_hook aborted. Renamed `,delve scavenge`'s `salvage` alias to `rummage` (still in the wandering-the-ruins flavour). Aliases now: `forage` / `wander` / `rummage`. +- **All 9 dungeon RELIC contract deploys were failing with `CheckViolationError`**: Migration 0176 created `item_contracts.kind` with a 14-kind CHECK allowlist; migration 0182 mirrored it onto `item_instances`. Both lists were missing `relic`. Added migration `0214_item_kind_relic.sql` that drops + re-adds both check constraints with `relic` in the allowlist (idempotent -- safe to re-run on a DB that already has the original constraints). Without this, every `nft_bootstrap` relic deploy failed silently and the relic backfill couldn't mint anything; relics in JSONB inventories stayed un-listable. + +### New Features +- **One-fight-at-a-time blocker (buddy / wild / shelter events)**: Previously a player could start a buddy PvP, then run `,delve battle`, then click Challenge on a fish wild buddy — three fight surfaces resolving against the same active buddy / HP, with battles stepping on each other's writes. New `services/fight_lock.py` is the single semaphore: at most one row per (guild, user) in the new `active_fight_locks` table (migration 0213). Entry-point commands acquire the lock; resolution paths release. Stale locks past their 8-minute TTL are automatically stolen on the next acquire so a crashed view can never trap a player permanently. Wired into `,buddy battle` (PvP entry), `,farm battle` (full `fight_lock_guard` async-ctx wrap), and `,delve battle` (entry-only). New `,admin fightlock peek ` shows the current lock; `,admin fightlock clear ` force-clears a stuck one. Kinds in use today: `buddy_pvp`, `fish_wild`, `delve_wild`, `farm_wild`, `escape_event`. Wild-fight Challenge buttons in the fish/delve/escape interactive views are not yet wired to the lock -- they'll be a follow-up; the entry-command coverage is the higher-impact half. + +### Bug Fixes +- **Every dungeon item, mat, consumable, weapon, armor, junk, and relic is now listable on the auction house**: Multiple compounding bugs trapped items in player inventories. (1) `services/nft_bootstrap.py` never deployed contracts for `dungeon_config.JUNK` (mats / salvage like `enchanted_thread`, `runed_chip`, `dragon_scale`) or `dungeon_config.RELICS` -- both catalogs walked by the bootstrap now. (2) `services/auction.SUPPORTED_KINDS` was missing `bait`, `junk`, `relic`, `stone`, `shop` -- so even items with deployed contracts were refused with "kind not supported"; all five added with sensible per-kind default currencies (RUNE for junk/relic, USDC for stone/shop, REEL for bait). (3) The biggest fix: dungeon drop paths (`_credit_loot_drop` for boss weapons/armor, `_credit_junk` for combat junk, relic-award branches in shrines/scavenges) only update JSONB count maps and never mint NFTs, so items like `phoenix_talon` could be in your inventory for weeks with no token to list. New `services/items.lazy_mint_from_jsonb` is wired into `find_owned_token_for_contract`: when `,ah list ` resolves a contract but no NFT exists, it walks the JSONB inventory (weapons_owned, armor_owned, consumables, junk_inventory, relics_owned, bait_inventory, crop_inventory, crafted_inventory) under a single `db.atomic()` block, decrements one count, and mints one token in the same transaction so the inventory and the NFT layer never drift. Existing items become listable immediately on first attempt; future drops are caught the same way without changing every drop site. The one-shot historical backfill (`,admin items backfill`) is extended to cover dungeon_junk and relics for operators who want to mint everything up front instead. + +### New Features +- **`,fish beachcomb` -- free 10-minute shoreline wander**: Mirrors `,farm forage` in shape and feel, themed for fishing. Wander the low-tide shore for a randomized payout. Outcomes: small/big LURE purse, small/big REEL kicker, a varied stash of bait packets, an occasional **Soggy Treasure Map** dropped straight to your junk inventory (so it feeds back into `,fish dig`), and on a rare **Ancient Relic** jackpot (~2%) a max-weight legendary fish drops directly into your fish_inventory ready to sell. Free roll, no inputs consumed; 10-min DB-clock cooldown on `user_fishing.last_beachcomb_at`. Send-then-edit animation with a "combing the shoreline..." pre-frame and a per-outcome reveal frame matching the farm-forage cadence. Aliases: `comb` / `shore` / `scavenge` / `wander`. Inflation-style oracle move on LURE/REEL credits is rendered on the receipt the same way `,fish dig` shows it. +- **`,delve scavenge` -- free 10-minute surface-ruin wander**: Same farm-forage shape, themed for the dungeon. Walk the surface ruins between runs for a randomized payout. Outcomes: small/big RUNE purse, small/big ORE pile (symbol weighted toward COPPER for small piles, SILVER/GOLD for the big pile -- without trivialising in-run mining), a cache of low-mid consumables (potion, charm, pickaxe oil, rune lure), a sealed Escape Scroll, and on a rare **Relic Shard** jackpot (~2%) one common/uncommon relic goes straight into your `relics_owned` bag. Refuses to run mid-delve (`run_id IS NOT NULL`) so it stays an out-of-run wander, not an in-run shortcut. Free roll, 10-min DB-clock cooldown on `user_dungeon.last_scavenge_at`. Aliases: `forage` / `wander` / `salvage`. + +### Bug Fixes +- **Group leaderboards completely rewritten** -- the previous `,group lb reserves` only counted the treasury cash columns and the previous `,group lb mktcap` measured **circulating supply** (a market property of the token, not "what does this group own"). Both were wrong relative to "rank groups by what they actually control." The new lb walks `group_lp_positions` joined to `pools` once for the guild and rolls up per-group: USD vault, BTC/SUN reserves valued at the oracle, the group's own token vault balance, AND their pro-rata share of every LP position they've contributed to (treating both legs of each pool). Three views now: `,group lb` / `,group lb value` (combined USD value of treasury + LP), `,group lb usd` (USD-denominated holdings only -- USD reserve + the non-own-token leg of each LP), `,group lb token` (own-token holdings -- vault_token_bal + own-token leg of each LP). A group that's $0 cash but holds 1M of its own token now ranks accurately on the token board; a group with $50k cash and zero LP ranks on the USD board even with no token. Aliases for back-compat: `reserves`/`treasury`/`vault` -> `usd`; `mktcap`/`mcap` removed (it was measuring a different thing entirely -- if you want a real circulating-supply mkt cap leaderboard, ask and I'll add it as a separate command). + +- **`,stake aave all` and `,stake dsy all` no longer trip "have X but need X" off-by-1**: The Safety Module deposit path read the user's raw wallet balance, converted it to a human float via `to_human()`, then passed that float back to `stake_sm()`, which called `to_raw()` on it. For balances that don't have an exact float representation (anything past ~15 significant digits, including most yield-accrued values), the raw -> human -> raw roundtrip drifts by ±1-2 raw units. The wallet check then compared `bal_raw < amount_raw` and rejected the stake even though the user owned exactly the displayed balance. Fix: `_sm_deposit` now passes the raw integer straight through to `stake_sm()` via a new `amount_raw` kwarg, bypassing the float roundtrip entirely. Manual / numeric stake amounts are unaffected. + +### New Features +- **`,admin createpool [seed_usd]`** -- mirror of `,admin removepool`. Auto-creates an AMM pool for any token pair with `$10,000` per side at oracle prices (override the seed amount with the third argument). Refuses to clobber an existing pool; falls back to `Config.TOKENS[sym].start_price` then $1 if the oracle has no price for an exotic side, so seeding works even before a token has its first trade. +- **`,admin groups fixpools`** -- repairs mining groups that ended up half-configured (no `token_network` bound, OR no tradable pools at all). Only touches broken groups: forces a token symbol if missing (4-5 alpha chars from the group name, fallback random), randomises `token_network` between Sun Network and Bitcoin Network, then creates the standard vault pools against **MBTC + MSUN + MOON**, seeded at `$Config.GROUP_VAULT_POOL_SEED_USD` per side at oracle prices. Skips groups that already have at least one tradable pool. Pass `dry` to preview the plan without writing. Idempotent -- pools that already exist are left alone, so re-running is safe. +- **`,group lb reserves` -- group treasury leaderboard**: Top mining groups in the server ranked by treasury USD (sum of `reserve_usd`, `reserve_btc * BTC_price`, `reserve_sun * SUN_price`). Paginated, top 3 get medals, breakdown shown per row so a group with $50k of mining buffer reads differently from a group with $50k of stablecoin reserves. Aliases: `,group leaderboard`, `,group rankings`, `,group top`. Bare `,group lb` defaults here. +- **`,group lb mktcap` -- group token market-cap leaderboard**: Top group tokens in the server ranked by `circulating_supply * price`. Shows mcap, supply, current price per row. Useful for spotting groups whose token has actually accumulated trading volume vs. groups with a registered token but no real market. +- **Hardcoded `1467740704725012638` as the default host guild**: `Config.HOST_GUILD_ID` now defaults to the operator's home server (1467740704725012638) instead of `0`, so every premium feature is auto-unlocked there even before any env var is set. Self-hosted deployments can still override via `HOST_GUILD_ID` in `.env`. +- **`,admin premium gift [message]`** -- bot-owner command that grants premium AND posts a celebratory embed in the recipient guild's system channel (or first writable text channel; falls back to DMing the guild owner). Use it for giveaways, friend-of-the-bot perks, or apologies for outages. Audit row is logged as `premium.gift` (distinct from administrative `premium.grant`) and `,premium status` shows `Source: gift` so the gifted server knows they were gifted. Identical write path to `,admin premium grant`, just with the recipient notification baked in. +- **Premium events stream into the `staff_audit_log`**: Every entitlement write (`grant`, `revoke`, `link_paypal_subscription`, `expire_overdue`, gift) now emits a row to the host guild's `staff_audit_log` via `core.framework.staff_audit.log_staff_action`. Severity is wired up (`warn` for grants/gifts, `danger` for revokes / paypal cancellations, `info` for renewals + sweep summaries). Action names are stable (`premium.grant`, `premium.gift`, `premium.revoke`, `premium.paypal_active`, `premium.paypal_cancelled`, `premium.expire_sweep`) so a future dashboard view can filter on them. Audit is best-effort and never blocks the entitlement write -- a logging failure shows up in the bot logs but doesn't roll back the grant. +- **Admin REST API for premium subscriptions** (`/api/v2/admin/premium/...`): Bot-owner-only (new `require_bot_owner` dependency; guild admins are explicitly NOT allowed since they could grant their own server premium for free). Endpoints: `GET /guilds` (list), `GET /guilds/{gid}` (status), `POST /guilds/{gid}/grant`, `POST /guilds/{gid}/gift`, `POST /guilds/{gid}/revoke`, `POST /guilds/{gid}/link` (manually attach a PayPal subscription_id), `POST /guilds/{gid}/sync` (re-fetch live state from PayPal), `POST /expire` (run the overdue sweep), `GET /webhooks?only_unprocessed=true` (recent `paypal_webhook_events` rows for diagnosing missed activations), `GET /features` (the same dict `,premium features` shows). Mirrors every Discord-side `,admin premium ...` command so the dashboard can do everything chat can. All writes route through `services/entitlements.py`, so the staff-audit logging fires identically regardless of whether the mutation came from chat or HTTP. +- **Premium gate scrub: farming, ,ask, AI replies/mentions, DiscoAI, chat refresh**: Closes the cost-leak gap left by the first premium pass. `cogs/farming.py` now requires premium (it's a buddy-game minigame in the same bucket as fishing / crafting / delves). `,ask` is decorated with `@premium_required("ai")` so non-premium users hit the standard locked-feature card. The `handle_ai_reply` and `handle_ai_mention` dispatchers in `cogs/help.py` (called from `bot.on_message` outside the command framework, so the decorator can't catch them) now check `entitlements.is_premium` at the top and silently skip non-premium guilds rather than spamming a "premium feature" card on every channel reply. `cogs/disco_ai.py` got a `cog_check` so all 4 hybrid_commands are gated. `cogs/chat.py` background memory refresh loop -- which previously walked every guild and called `ai_complete()` for each -- now skips non-premium guilds entirely so we never burn token budget on a server that hasn't paid (the host server still refreshes via the `is_host_guild` override). Added `farming` to `PREMIUM_FEATURES` so `,premium info` and `,premium features` advertise it. +- **Owner-side PayPal recovery**: `,admin premium link ` manually attaches a PayPal subscription to a guild and pulls the live status from PayPal -- use when a webhook delivery was dropped (PayPal outage, server restart) and a guild's payment cleared but they're still showing as non-premium. `,admin premium sync [guild_id]` re-fetches PayPal state for every PayPal-linked guild (or just one) and writes the deltas through, so a plan migration or support-ticket reactivation can be reconciled without waiting for the next webhook. +- **Auto welcome on guild join**: When the bot is added to a new server, `cogs/premium.py` now drops a welcome embed in the system channel (or first writable text channel; falls back to DMing the guild owner) explaining what's free everywhere, what's premium, and how to subscribe via PayPal. The host guild is skipped since it doesn't need a sales pitch. Eliminates the "I added the bot but nothing happened" experience for new operators. +- **Per-guild premium subscriptions with PayPal**: Discoin is now a shared multi-tenant bot -- any guild can invite it, but the cost-heavy features are gated behind a per-server premium subscription that the server owner pays via PayPal. Trading, gambling, bank, profile, drops, jobs, work, daily, gambling, and basic buddy management (hatch / rename / storage / shop / BUD economy / staking / shelter / leaderboard / species) stay free everywhere. **Premium-only**: AI (`,ai`, `,disco`, agents, plugins, web search), fishing, crafting, delves, expeditions, buddy battles + arena, buddy breeding (nest/daycare/eggs), the buddy auction house (list / buy / market / gift), and `,buddy talk`. Server admins run `,premium info` to see plans and `,premium subscribe` to get a PayPal approval link; once payment clears the webhook flips premium on for everyone in that server. The host server (`HOST_GUILD_ID` env var on Railway) is auto-unlocked, and the bot owner can manually grant / revoke / list premium for any guild via `,admin premium grant [days]` from the host server -- so you can keep your home server fully unlocked without paying yourself. PayPal Subscriptions API integration is sandbox-ready out of the box; webhook at `POST /api/v2/paypal/webhook` verifies signatures through PayPal and is idempotent via a `paypal_webhook_events` ledger. Locked-out players see one consistent gold "Premium Feature" card pointing at `,premium info` instead of a generic error. +- **Capture button now appears DURING fishing wild-buddy fights**: `,fish` wild battles now show a `Capture (NN%)` affordance on row 1 alongside Strike / Special / Brace / Risky -- mirroring the delve pattern. Disabled while the wild buddy is above `CAPTURE_HP_THRESHOLD` (30% HP); enables below it with the live capture chance on the label so players can read whether to throw now or wear the wild buddy down further. Capture odds use the same shape as delves (`CAPTURE_BASE_CHANCE - tier_penalty + (1 - hp_frac/threshold) * 0.5`). On success the wild buddy is inserted into `cc_buddies` immediately and the battle finalises as a captured win; on failure the wild buddy gets a free turn and the fight continues. `services.fishing.resolve_wild_battle` gained a `skip_capture_roll` kwarg so the manual + auto-roll paths can never double-insert. +- **Work payout breakdown button**: Every `,work` receipt now ships with an `ℹ️ What boosts my pay?` button that pops an ephemeral panel explaining every multiplier that touched the payout -- user-created LP (with the player's live LP USD value and current bonus %), the progressive tax band and rate, stones (Hash/Lock/Vault/Liq + meta stones), buddy companion, group hall Gilded Arch, Rugpull King -- plus the per-job daily cap, daily-streak cooldown reduction, and the order of operations the engine actually applies. Centralises an explanation that previously lived only in the source code, so players can see exactly why the receipt looks the way it does. + +### Changes +- **Pre-fight `Try Capture` button removed from every game**: The one-shot 20% capture-without-fighting affordance was the cheap way to skip the engagement and trivialised wild battles -- it's gone from `,fish` wild-buddy encounters and from the world-event escaped-buddy prompts. The fishing path now opens straight into Challenge (with the new in-fight Capture button as the engagement reward); the escaped-buddy event keeps just Challenge so claiming a buddy always means winning the fight first. +- **AAVE and DSY stake panels now show USD next to every crypto amount, with a how-it-works footer**: `,stake aave` and `,stake dsy` (status + empty-state) now render `staked_h TOKEN ($USD)` on the Staked / Pending Yield / Daily Rate / Total Staked rows, pricing each amount through the live oracle so the player can read their position in dollars without doing the multiplication. The footer is replaced with a one-line explanation: stake X to earn ~Y% APY in the paired stablecoin (USDC for AAVE, DSD for DSY), unstake -> Nh cooldown -> withdraw, up to 10% can be slashed in a shortfall, USD shown is current oracle value (not principal-protected). Yield-token USD pins to $1 for USDC/DSD and reads from oracle for any non-stable LM tokens. + +### Bug Fixes +- **`,fish sell ` / `,fish sell rod` / `,fish sell traps` no longer crash with `'Frozenset object has no attribute get'`**: All three REEL-refund paths in `services/fishing.py` had `_Cfg.EARN_ONLY_TOKENS.get("REEL", {}).get("network", "dsc")`, but `EARN_ONLY_TOKENS` is a `frozenset` of token symbol strings -- it has no `.get()` so the line raised on every call. Even if the lookup had worked, the fallback `"dsc"` was the wrong network short anyway: REEL lives on the Lure Network and `wallet_holdings` keys on `"lur"` (see `fishing_config.LURE_NETWORK_SHORT`), so a `"dsc"` write would have silently bypassed the player's REEL balance. All three call sites now hardcode `fc.LURE_NETWORK_SHORT` since REEL's home network is fixed by domain. + +### Changes +- **Safety Module yield bumped from ~20% APY to ~50% APY, LM rewards 5x'd**: `Config.SAFETY_MODULE.AAVE.daily_yield` and `DSY.daily_yield` are now 0.001370 (was 0.000548) so AAVE and DSY stakers earn ~50% APY in USDC / DSD on the USD value of their stake. The `lm_daily` for stablecoin savers is bumped to 0.000137 (was 0.000027), giving USD savers ~5% APY worth of AAVE on top of normal savings interest. Slash risk (10% on shortfall) and the 24h unstake cooldown are unchanged. Audit confirmed the yield path is still working: `apply_sm_yield` only fires from `,stake aave claim` / `,stake dsy claim` (no silent auto-tick), the "skip the upsert when yield rounds to 0 raw" guard is still in place so accrual keeps building until claimable, and every player-facing APY number reads from config so the new rate shows up everywhere automatically. + +## [main] -- 2026-05-05 + +### New Features +- **`,ai recontext` rebuilds Disco's context from scratch when it loops on stale info**: `,ai forget` only wipes the one-line memory summary, but Disco also reads from conversation history (last 14 turns), inferred traits, reaction-category counters, long-term facts, and the Redis short-term buffer -- any of which can carry a hallucination forward across turns. The new command has three scopes: `,ai recontext` (no arg, anyone) drops the caller's per-user state and walks Redis for every channel's short-term buffer for that user; `,ai recontext @user` (Manage Server) does the same wipe for someone else; `,ai recontext server` / `all` / `everything` (Manage Server, confirmation gated) is the NUCLEAR option -- wipes per-user state for EVERY user in the guild, clears the recent server-events drama feed and the channel-context (reactions / edits / deletes / banter) feed, deletes guild-scoped DiscoAI facts and episodes, and walks Redis for every channel's short-term buffer; `,ai recontext channel` / `here` (Manage Server) drops just this channel's banter feed and short-term buffers when the loop is localised. Configuration tables (custom prompts, AI-channel allowlist, model picks, lore facts) are NEVER touched -- a memory wipe should not silently undo guild settings. Aliases: `,ai refresh`, `,ai resync`, `,ai rebuild`. Reply embeds a per-table breakdown of what was actually deleted. +- **Disco's responses now adapt to channel topic, time of day, and server vibe**: Every AI reply path (`,ask`, replies to AI messages, @mentions, ambient chatter) now picks up the Discord channel's topic, a tone tag inferred from the channel name (lounge / market / degen / shitpost / tech / support / news), the current UTC time-of-day slot (morning / afternoon / evening / late-night / graveyard-shift), the active market event + a bull/bear/crab regime label derived from token price moves, and a channel-temperature read (HYPED / SPICY / QUIET / CHATTY). Players in #serious-help get a calmer Disco; #degen gets a chaotic one; gm at 3am UTC reads as suspicious instead of cheerful. None of this required a new prompt -- it's all derived from data Discord and the bot already had. +- **Disco recognises staff, mods, regulars, patrons, and newcomers from role names**: A new role-tag pass scans the user's Discord roles for keywords (founder/owner/admin -> "staff", mod/helper -> "mod", og/legend/veteran -> "regular", whale/patron/booster -> "patron", new/noob/rookie -> "newcomer") and tells the model to calibrate snark vs respect accordingly. Staff don't get publicly roasted, newcomers get extra patience. +- **Disco surfaces what happened to YOU recently, not just guild-wide drama**: A new "RECENT EVENTS FOR " block filters the existing server-events feed to the asking user so questions like "did I win earlier?" or "why am I broke?" land with concrete personal context instead of generic answers. + +### Bug Fixes +- **Custom emojis no longer render as monospace text inside backticks**: Disco kept emitting markup like `` `<:peepoSip:1468239419856261253>` `` -- a complete, valid emoji wrapped in inline code. Discord renders backticked content in monospace and skips emoji substitution, so it showed as literal characters in the channel. Root cause was the system prompt itself: the emoji-context block told the model to "paste the raw `<:name:id>` markup EXACTLY as shown" with backticks around the example, and the model was reproducing the wrapper verbatim. Prompt rewritten to describe the markup without backtick examples and to spell out that the markup must be naked text. Belt-and-braces: a new `_BACKTICKED_EMOJI_RE` pass in `core/framework/ai/emoji_safety.repair_custom_emojis` unwraps any single- or double-backticked emoji markup before the rest of sanitisation runs, so even if the model still wraps it the user sees the real emoji. +- **SUN mining tick no longer crashes with `NameError: name 'symbol' is not defined`**: `_process_sun_guild` was calling `_mint_group_vault_tokens(..., mining_chain=symbol)` but `symbol` was never a parameter of that function -- it lives on the parent `_process_pow_guild` and never reached this scope. Group vault minting silently failed every SUN tick. Hardcoded to `"SUN"` since this branch only runs for SUN by design. +- **Custom emojis no longer render as raw ``, Discord rendered the literal opening run as garbage in the middle of the reply. New `core/framework/ai/emoji_safety.repair_custom_emojis` runs on every AI reply (ask / reply / mention / ambient), strips any unclosed `` works as the prefix-command equivalent for typing players. Cooldowns track per-ability via the existing `player_buffs` JSONB so no migration is needed. +- **Unified delve inventory and protected `,delve sell all`**: `,delve inv` now lists weapons, armor, consumables, and junk in one rarity-sorted panel (legendaries float to the top of every section, with per-stack salvage value on junk). `,delve sell all` was the foot-gun: it used to liquidate every owned non-equipped piece of gear. It now ONLY dumps salvage-tier junk -- crafting mats, usables, consumables, weapons, and armor are all protected and have to be sold individually via `,delve sell ` or `,delve junk sell `. Junk's own `,delve junk sell all` is also salvage-only now so you can never accidentally dump a rare mat or charm in a bulk sweep. + +- **`,delve stake all` / `,delve unstake all` / `,delve sell all` bulk commands**: One-shot helpers replace the per-ore / per-item grind. `stake all` locks the entire COPPER + SILVER + GOLD wallet in a single call (skipping any ore you have 0 of); `unstake all` pulls every staked ore back to wallet and pays out accrued RUNE in the same swing; `sell all` dumps every owned non-equipped, non-starter weapon and armor piece for 50% RUNE refund and reports the total. Each command returns one summary embed with per-ore / per-item lines instead of N receipts. +- **33 new Delve weapons and armor pieces fill the mid/late tier gaps**: Every class weapon line and armor line now has an entry at every tier from start through tier 11. Rogue's shortsword line went from 3 entries (rusty / iron / phoenix) to 12 -- silvered_dirk (T2), venom_kiss (T3), shadowstrike (T4), moonlit_kris (T5), nightshade_blade (T6), abyssal_dirk (T8), wyrmfang_kris (T9), starshard_dirk (T10), and first_fang (T11) close the brutal jump that left Rogues stuck on +3 ATK gear into floor 35+ bosses. Archer bows gained T6 / T8 / T10 (composite_bow, voidstring_bow, archon_bow) and crossbows filled T1, T3, T5, T7, T9, T11. Mage staves gained T4, T6, T8, T10. Druid rods gained T1, T3, T5, T7, T9. Medium armor (Rogue/Archer) gained T6, T8, T10 and light armor (Mage/Druid) gained T2, T4, T6, T8, T10 -- so non-Warrior classes can finally buy gear that keeps pace with the depth-scale on their way to The Archon. + +### Changes +- **Group reserve embed and help are clearer**: `,group reserve` now leads with a one-line description of what the reserve is for (Hall upgrades, LP seeding), collapses the three side-by-side "USD Bucket / BTC Bucket / Token Vault" fields into a single **Balance** breakdown, renames the ambiguous "Mining Cut" to **Reserve Rate**, and renames "Active Inflows" to **How It Grows**. The footer now shows the full configure / spend commands instead of cropped hints. The Reserve help category in the `,group` interactive menu was rewritten to spell out the three income sources up front, and the `,group set` help no longer advertises the unsupported `reserve_pct=` key (founders set the rate via `,group reserve set <0-100>`). `,group info` drops the partial / inconsistent "BTC Bucket" field and just points at `,group reserve` for the breakdown. + +### Bug Fixes +- **Pump patterns chaos / volatile / spike / bull / bear now actually move the chart**: Several `,admin pump` patterns and the auto-pump scheduler were rolling but the chart looked dead. `chaos` was an unbounded random walk × 2.2 that could push the multiplier negative (clipped to 1e-12) on a 1.2σ draw -- now bounded to ±mag via tanh and edge-tapered so it round-trips near the start. `volatile` had a 10% net drift (a 30% pump closed at +3%) -- bumped to 30% drift with wider tapered swings. `spike` was 0.04 wide in t-units, ~4 ticks at PRICE_TICK_SECONDS=15s -- widened to 0.10 so the wick survives 5m/15m candle aggregation. `bull` and `bear` had a non-tapered `sin(5πt)` overlay that overwhelmed the trend in the first ~10% (a "bear" pump opened *up* at t=0.05) -- the overlay is now edge-tapered and shifted to 4π so the early phase reads cleanly directional. +- **Live admin pump events survive bot restarts**: `_admin_price_events` was an in-memory dict, so every container deploy or restart silently dropped active pumps and the chart froze where the last drift tick had landed it. Added migration `0210_admin_price_events.sql` and rehydration in `Trade.before_drift` so events resume from where they paused; the auto-pump scheduler, manual `,admin pump`, and the cancel path all read/write through the new table. +- **`,buddy species` no longer trips Discord's char-max limits**: the per-tier field chunker could overflow the 1024-char field cap once a tier filled up, and a single ability_desc longer than the cap would commit an empty field; both cases now budget against the 1024-per-field and 6000-per-embed limits with a hard cap as defence-in-depth and a "+N hidden" tail when content would otherwise spill over. Also caps the comma-joined "available species" hint that `,buddy swap` shows when called with no/unknown species, since the full catalog had crept close to the field limit. +- **Reserve total in upgrade list / hall info now includes the token vault**: `,group upgrade list` showed a footer reserve number that excluded the group token vault, and `,group hall info` did the same -- so groups with most of their value in their token vault saw a smaller "Reserve" than what `,group reserve` reported and couldn't tell which upgrades they could actually buy. All three displays now go through one helper (`_spendable_reserve_total`) so the numbers can never drift again. The upgrade list also breaks the total down per bucket and is shown even before any upgrades have been purchased. +- **Hall upgrades can now spend the group token vault**: `,group upgrade buy` previously only counted the USD and BTC reserve buckets, so groups whose token vault held most of their value were stuck unable to spend it on upgrades despite the reserve embed showing the full total. The buy now drains in priority USD -> BTC -> token vault at live oracle prices, the upgrade list affordability check matches, and the receipt prints the per-bucket spend. + +### New Features +- **Starter buddy gear shop (3 tiers, DSD-priced)**: New `,buddy gear shop` lists basic gear sold for cash -- always in stock, no recipes. Apprentice line ($1k, +2% stat) for ATK / HP / SPD plus a $200 collar accessory; Initiate line ($5k, +4% stat) plus a $1.5k pin; Adept line ($25k, +6% stat or focused effect like crit chance / DR) plus an $8k medallion. Buy with `,buddy gear buy ` -- deducts wallet+bank, equips on the active buddy, and warns before replacing existing gear. Crafted gear is still stronger; the starter ladder is a baseline so new players have a way to gear up before unlocking forge / void / battle charms. +- **Numeric emojis are now valid amounts**: Commands that take an amount (`,dice`, `,deposit`, `,withdraw`, `,transfer`, etc.) now accept `💯` as 100, `🔟` as 10, and keycap-digit sequences like `1️⃣0️⃣0️⃣` as 100. Works alongside `$`, `k`/`m`/`b` suffixes, and `all` -- so `,dice $💯` bets $100. Unknown emojis still produce the normal "amount must be a number" error rather than crashing the parser. +- **5 new buddy species with unique abilities**: Tortuga (turtle, Fortress Shell -- reflects 12% damage and -15% damage taken), Jolt (electric mouse, Static Shock -- 30% chance for +50% damage discharge per hit), Phantom (ghost, Phase Shift -- 30% dodge that reflects 25% ATK back), Verdant (sprout, Photo Synth -- 1.5%/round regen and slow ATK ramp), and Mimik (treasure chest, Ambush -- first hit guaranteed crit + permanent +20% crit chance). Each ships with full mood-frame ASCII art, dialogue, and signature bonus lanes. +- **Level-gated buddy abilities (Lv 15 + Lv 30 unlocks)**: every species now has a Secondary at Lv 15 and a Tertiary at Lv 30 from a shared kit (Sharp Claws, Tough Hide, Evasive, Lucky Strike, Swift Recovery, Battle Focus, Iron Will, Second Wind, Killing Blow, Berserker, Affinity). Both ramps fire across the auto-resolve engine, PvP, wild captures, and arena -- the same kit on every battle surface. The buddy panel and species roster show locked / unlocked state per slot. +- **12 more buddy gear items**: 4 cosmetic accessories (Tiny Top Hat, Cozy Scarf, Tiny Glasses, Moon Pendant) and 8 stat-bearing charms (Swiftness, Crit Focus, Warding, Bloodthorn, Bloomheart, Endurance, Aegis of Ward, Treasure Compass, Harvest, Tide Pearl). Battle charms now actually take effect -- Fighter.from_row reads the equipped charm's stat_bonus dict on every fight, so existing charms (Battle, Vitality) and the new ones layer onto ATK / HP / SPD / crit / DR / lifesteal / regen / reflect. +- **Group system refresh: 14 new Hall upgrades across 5 lines**: Hall upgrades grew from 8 to 22, with two new lines (Industry, Tribute) plus deeper tiers in Atmosphere (T4-T5) and Expansion (T3-T4). Industry adds Angler's Dock / Greenhouse Wing / Delve Bastion / Forge Workshop / Guild Market / Master Industries -- group-wide bonuses on fishing, farming, delves, and crafting cashouts that apply to every member anywhere, not just inside the Hall. Capstones go up to **+15% / +20%** payouts. +- **Tribute upgrades make the group reserve grow from member productivity**: Buy Tithe Box, Deep Coffer, Guild Mint, or Sovereign Treasury and every fishing / farming / delve / crafting cashout your members make pays a system-funded grant into the group's reserve_usd. No tax on the user -- the grant is a guild perk. Sovereign Treasury stacks to a 4% reserve drip per cashout across all four tracks. Fixes the stale-reserve problem: the reserve now grows even when no one is mining. +- **Group Industry payout bonuses**: Members of a group with the matching upgrade earn +5% (Tier 1), +10% (Guild Market), or +20% (Master Industries) on every fishing, farming, delve, and crafting cashout. Bonuses apply group-wide -- you don't have to be in the Hall thread. +- **`,group reserve` now lists every active inflow**: The reserve embed now shows a per-source breakdown of how the reserve is growing -- mining cut, every active tribute %, and LP yield -- so founders can see exactly where the next dollar is coming from. +- **Crafting expansion: 20 new recipes spanning Lv2-45**: Eleven new general-tier and specialty-locked recipes (cooking-routed bait, smithing/farming/dungeon ammo, gambit chips, harvest grimoires, molten quench oil, manta-tier lures, mid-tier buddy XP food) thicken the crafting ladder so newer crafters find more steps between Lv5 and Lv25 and dedicated specialists have fresh late-game targets. +- **Eight new buddy gear pieces from crafting expansion**: Three accessories (Silken Ribbon, Mosaic Scarf, Forgemaster Band) and five charms (Iron Band +5% HP, Mossy Amulet +5% XP, Sunbeam Locket +10% expedition loot, Tempest Amulet +12% ATK, Forge-Seal Pendant +8% ATK / +8% HP) extend the gear ladder from a Lv6 entry charm through epic and legendary tiers, giving buddy battlers and expedition runners more meaningful gear choices. +- **Forge-Sealed recipes give FORGE a real sink**: Three prestige recipes (Forgemaster Band Lv28, Forge-Seal Pendant Lv30, Forgeheart Elixir Lv45) consume FORGE coin directly via the existing token/ input parser alongside their FGD fee. Players who stake INGOT into FORGE yield can now cash that yield into permanent buddy gear and a legendary buddy XP-surge elixir instead of being forced through the one-way USD cashout. +- **6 new fishing zones with distinct mechanics**: Tidal Pool, Mangrove Thicket, Sunken Galleon, Bioluminescent Bay, Crystal Caverns, Storm Surge -- each with unique junk/rare tuning. Total zones now 24. +- **38 new fish and crabs**: 20 fish filling out sparse zones (Sewer, Tidal Pool, Ocean, Swamp, Reef/Kelp, Glacier, Trench, Moonpool, Storm Surge, Mangrove) plus 8 new trap-only crabs: Horseshoe Crab, Ghost Crab, Mantis Shrimp, Crystal Crab, Storm Crab, Fire Vent Crab, Moon Phase Crab, and Nebula Crab (legendary). +- **Fish facts on every catch**: The catch embed now shows a random short fact about the species you reeled in. All ~85 fish and crabs have 5 rotating facts each so the details stay fresh for repeat catches. + +### Changes +- **Healing buddy rebalance**: Wecco's Preen now heals to 50% HP (down from 60%) and buffs ATK +12% (down from 15%); trigger threshold tightened to <30% HP. Gloomer's Lunar Regen heals 2%/round (down from 3%) and stops once HP is above 75% so it can't passive-stack at full health. Blazer's Flame Drain siphons 15% of damage dealt (down from 20%). All heals (lifesteal, brace, regen, preen, second wind) now share a per-fight soft cap (1.20x max HP) -- once a fighter exceeds that, further heals are halved. The Lv 30 Second Wind tertiary (one-shot 25% heal) compensates at higher levels for the per-trigger nerfs. + +### Bug Fixes +- **Block storing a buddy that's currently on an expedition**: `,buddy storage` deposit no longer accepts a buddy whose expedition is still running -- the storage drop made the buddy "owned but absent" and let players sidestep the expedition wait. The guard lives in `services/buddy_lifecycle.to_storage` so every caller (dropdown, future slash, scripts) is covered. +- **Hide gross from receipt when it equals credited amount**: When slippage is 0 (or so small it rounds to the same 2dp display),; the cashout receipt showed both "(gross $X)" and "Credited: $X" with; identical formatted values, making the embed look like a double payout. (`71da55d6`) +- **Rebalance tail-loaded curves so chart actually moves**: Auto-pump events (distribute / accumulate / moon / pump / dump) were; firing correctly -- _admin_price_events writes wired through to; update_price + upsert_candle without issue -- but the curves themselves (`22133a1a`) + +### Changes +- **Unify AAVE/DSY staking under `,stake`**: The standalone `,aave` and `,dsy` command groups are gone. Use `,stake aave deposit|unstake|withdraw|claim|status` and `,stake dsy ...` instead, so all yield-bearing positions (yield farms, validators, Safety Module) live under one command tree. `,stake mine` now shows a Safety Module section with each AAVE/DSY position alongside your farm and Lunar Mint deposits. +- **Safety Module value now counts toward net worth**: AAVE and DSY positions (staked tokens at oracle + accrued pending yield) feed into `compute_net_worth`, so they show up in `,bals` summary, the leaderboard, and GDP. The `,bals nodes` view and the DRS profile audit also gained a Safety Module section so a player's full staking footprint is visible in one place. + +### Discord Bot +- **Redirect ,aave/dsy stake status to the status embed**: "status", "info", "pos" as the amount arg now show the Safety Module; position embed instead of erroring, matching user intent. (`e4a84ee0`) +- **Add buddy storage surrender; fix aave/dsy stake float error**: - Add Surrender button to buddy storage panel (row 2, danger style).; Clicking it switches dropdown to _BuddyStorageSurrenderSelect; selecting; a stored buddy sends an ephemeral yes/no confirmation before calling (`a5a3efb1`) +- **Halve swap price impact and nerf moon event drift**: - PRICE_IMPACT_DIVISOR: 2_500_000 -> 5_000_000 (buy/sell impact halved); - _SWAP_ORACLE_NUDGE_CAP: 0.05 -> 0.025 (AMM swap chart nudge halved); - Moon drift bias: 80%/30% -> 40%/15% daily (network coin / regular token) (`2d790739`) + +### Tests +- **Fix test_price_impact_too_large for new PRICE_IMPACT_DIVISOR**: PRICE_IMPACT_DIVISOR doubled (2.5M -> 5M), so the 50% impact cap now; triggers at $2.5M instead of $1.25M. Bump both buy and sell test trade; sizes from $2M to $3M (60% impact) so they still exceed the cap. (`1d0adec0`) + +### New Features +- **Realistic Aave Safety Module staking**: AAVE and DSY now behave like the real Aave protocol:; Safety Module (stake/earn):; - Stake AAVE in the Safety Module -> earn USDC yield (~5% APY) (`08696175`) + +--- + +## [main] — 2026-05-04 + +### New Features +- **Surrender stored buddies from storage panel**: The buddy storage panel now has a red Surrender button. Clicking it swaps the dropdown to a list of your stored buddies; selecting one shows an ephemeral yes/no confirmation before permanently sending them to the shelter. + +### Bug Fixes +- **Cashout receipt no longer shows a confusing "double payout"**: When burning tokens (REEL, RUNE, FORGE, HRV, BUD) with zero or near-zero slippage, the receipt used to show both `(gross $X)` and `Credited: $X` with the same formatted value, making it look like two separate payouts. The gross is now hidden when it formats identically to the credited amount -- it only appears when slippage is large enough to produce a visible difference. +- **,aave stake / ,dsy stake "all" and float conversion error**: `,aave stake all` and `,dsy stake all` now stake your full wallet balance instead of crashing with a float conversion error. Passing any non-numeric string (like "all" or "max") is handled cleanly; unknown strings get a friendly error message. + +### Changes +- **Halve swap price impact across the board**: `PRICE_IMPACT_DIVISOR` doubled (2.5M -> 5M) so buy/sell trades move prices half as far per dollar spent. AMM swap oracle nudge cap halved (5% -> 2.5%) so pool swaps push the chart price half as hard. Moon-event drift biases cut in half (80%/30% -> 40%/15% daily) and the moon volatility floor reduced (2x -> 1.5x), keeping moon events exciting without runaway pumps. + +### Maintenance +- **Default every DM toggle off, flip existing rows**: All dm_* prefs on user_prefs now DEFAULT FALSE -- players opt in via; ,notify on. Migration 0208 also rewrites every existing; row so already-registered players stop receiving DMs on deploy (`8686f3d8`) +- **Bump auto-rotation defaults to $10M / $5M**: Per-season prize pool default 5K -> 10M USD; per-challenge default; 500 -> 1M USD (5 challenges = 5M total challenge pool / pair).; MAX_POOL_USD ceiling raised 1M -> 100M so admins can keep scaling (`d48f2da7`) + +### New Features +- **AAVE and DSY Safety Module (realistic DeFi staking)**: AAVE and DSY now function like the real Aave Safety Module. Stake AAVE to earn USDC yield (~5% APY); stake DSY to earn DSD yield (~5% APY). A 24-hour unstake cooldown applies before withdrawal, and up to 10% of staked tokens can be slashed during a shortfall market event. USD savers also passively earn AAVE tokens as liquidity mining rewards (~1% APY). Commands: `,aave stake`, `,aave unstake`, `,aave withdraw`, `,aave claim`, `,aave status` (and `,dsy` equivalents). +- **Auto-rotating themed (season + 5 challenges) pairs**: ,season auto on flips a per-guild toggle that keeps a coherent themed; event running indefinitely. Each "pair" ships one season (with theme +; metric) and 5 themed challenges; the next pair starts when either: (`788a8575`) +- **2-round cooldown on healing potions + attack scrolls**: Stops players from chain-popping heals or damage scrolls back-to-back; during a fight. Both gates ride the existing player_buffs JSONB +; _tick_player_buffs lifecycle so no new column / migration is needed: (`3decb251`) +- **Hall prefixless toggle; harden ,ah browse 6000-char guard**: Two changes:; 1. ,group hall prefixless on/off/toggle (founder only); New per-group flag stored on mining_groups.hall_prefixless (`ec01d6c3`) +- **Anvilstone in FORGE; cosmetics craft-only with 1h timed roles**: Five batched changes from the latest playtest pass:; 1. Anvilstone now stakes in FORGE (was USD); The crafting meta-stone aligns with the network it boosts the same (`da561a3f`) +- **Three meta-economy stones (Gavelstone / Anvilstone / Chimerastone)**: USD-priced leveled gems that scale off cross-cutting bot systems; instead of a single minigame. Each carries the standard; ``work_daily_bonus`` per level on top of its surface-specific bonus. (`3516f54c`) +- **,admin cosmetic role config + fix buddy gear column name**: Per-guild cosmetic role overrides: guild_settings.cosmetic_role_overrides; JSONB (migration 0202). DB helpers get/set_cosmetic_role_override in; guilds.py. _use_cosmetic checks the guild override before falling back to (`1e6fec2d`) +- **Drs fish/farm subcommands + cosmetic buy fix**: Add ,drs fish @user and ,drs farm @user to helpers.py -- fishing and; farming audit profiles for DRS staff (level, XP, rod/plot tier, biggest; catch/harvest, total payout, last active). Results DMed for privacy; (`ab2e9963`) +- **Expeditions, farming, crafting, buddy gear, cosmetics, crab trap, changelog auto-post**: New expedition zones (Volcano lv15, Void lv20) with species affinities,; zone events, favoured loot tables. 24h expedition duration added.; Six new crops (pepper, mushroom, lavender, rose, crystalmint, dreamroot) (`d37c11b3`) + +### Bug Fixes +- **Vault levelup column id, currency display, leveling speed**: - ,inventory levelup vault no longer crashes with column "id" does not; exist -- the USD wallet debit now uses db.update_wallet() (right WHERE; user_id key) instead of a hand-written UPDATE. (`cc948b54`) +- **,buddy pools shows 🟢 LP-marker like ,trade pool list**: The buddy-network swap-pair list didn't surface which BUD<->X pairs; the player was earning burn-fee rewards on, even though; _distribute_burn_lp_reward (services/buddy_economy.py) pays a USD (`3f720777`) +- **Cosmetic-list crash, seeds back in farm shop, bulk-sell crab traps**: Three follow-ups from playtest reports:; 1. ,admin cosmetic list crashed with "dictionary update sequence; element #0 has length 1; 2 is required". get_cosmetic_role_overrides (`4a41f135`) +- **Buddy gear JSONB coercion + gear column name; feat: sell gear 50%; cosmetic role config**: Fix ,buddy panel: gear JSONB comes back as raw string from asyncpg --; pass through _as_dict() (from services.fishing) before handing to; gear_display(). Fixes 'str' object has no attribute get' error. (`7797199a`) +- **Ah buy crash -- compare epoch float to epoch float, not datetime**: buy_listing was comparing row["expires_at"] (an epoch float returned by; _coerce) against datetime.now(utc). Python rejects <= between float and; datetime, producing the "TypeError: '<=' not supported" the player saw. (`8885e35e`) + +### Refactoring +- **Rename ,group hall prefixless -> ,group hall prefix (inverted)**: Friendlier surface: instead of "prefixless on" (double-negative), the; command is now ``,group hall prefix on/off``:; ,group hall prefix on require the bot prefix (default) (`6ccc0646`) + +### Tests +- **Include cosmetic + weapon + armor in _VALID_APPLY_KINDS**: services.crafting.apply_crafted_item already routes weapon / armor; (legendary smithing recipes) and the new cosmetic / path through; the dispatch, but tests/test_crafting_config.py:_VALID_APPLY_KINDS (`3a87430d`) + +### Discord Bot +- **Stack identical AH listings from the same seller in browse + search**: Sellers who post N copies of the same thing -- 50 Healing Herbs at the; same price, 8 buddies sharing name / level / rarity / gender -- used to; get N separate rows in `,ah` and `,ah search`, pushing the rest of the (`b57f2f06`) +- **Fix delve Mine / Open buttons silently no-oping when no junk drops**: Both handlers passed `view=drop_view` directly into; `interaction.followup.send`, where `drop_view` is `None` for the common; no-junk-drop case. discord.py 2.3's `Webhook.send` raises `TypeError` on (`ac929ad4`) +- **Drop conflicting aliases on ,buddy egg subcommands**: - ,buddy egg panel had aliases=['storage','bank','banked']; - ,buddy egg deposit has aliases=['bank']; Both children of buddy_egg, so 'bank' clashed at registration time. (`9538f2a2`) +- **Egg ops accept any species from the global catalog, not just fishing**: Wolf / ember / other delve / farm / breeding eggs were rejected by; ,buddy egg sell / hatch / gift because the validation list was; hard-pinned to fc.FISHING_BUDDY_SPECIES (shrimp/crab/octopus/lobster/ (`465338fe`) +- **Sunset ,fish egg into ,buddy egg + pay FREN for egg sales**: - ,fish egg now redirects to ,buddy egg with the new command map.; - ,buddy egg adds hatch / sell / gift mirrors that delegate to; fish_svc, plus a 'panel' alias for the consolidated storage panel. (`15f4a572`) +- **Slot warning honors purchased slots + use 'active/storage' terminology**: slot_pressure now checks BOTH active (status='owned') and storage; (status='stored') caps, including purchased upgrades, and only fires; the warning when a capture would actually be refused -- i.e. both (`94b3b2e1`) +- **Route captures + hatches to storage when active full; auto-advance after mine; pin Junk row**: - Wild-buddy captures (delve charm + button) now use capture_destination; so they auto-route to storage when battle slots are full, and surface; 'went to storage' / 'battle + storage full' to the player. (`41ca92d5`) +- **Move Rest off row 0 of delve room view (6 > 5 width)**: The new Pray button (added when shrine rooms got their button) pushed; row 0 to six buttons (Next/Mine/Open/Pray/Descend/Rest), which Discord; rejected with 'item would not fit at row 0 (6 > 5 width)'. Bump Rest (`7447b186`) +- **Fix delve_upgrade hybrid command (no VAR_POSITIONAL)**: Discord hybrid commands can't take *args. Switched to a single; keyword-only string and split it inside the parser. (`4cd4fdc4`) + +--- + +## [main] -- 2026-05-03 + +### Bug Fixes +- **Auto-pump market events (`distribute` / `accumulate` / `moon` / `pump` / `dump`) actually move the chart now**: The price-tick wiring was correct -- the auto-pump scheduler in `cogs/admin.py` writes to the `_admin_price_events` dict, the per-tick drift loop in `cogs/trade.py:645` reads it, calls `services.chart_patterns.compute_price`, and persists the new oracle via `update_price` + `upsert_candle`. The chart looked dead anyway because the curves themselves were tail-loaded: `moon` was `t^3` (only 2.2% of magnitude delivered by 30% of the event, well under one tick of GBM noise); `pump` / `dump` were `t^1.8` (similar story); and `accumulate` / `distribute` returned pure noise around 1.0 for the first 70% of the window, only dumping/breaking out in the last 30%. So players watching the chart 5-10 minutes into a 60-minute moon saw zero movement and concluded the event was broken. Curves rebalanced for visible drift from the very first drift tick while preserving f(0)=1.0 and f(1)=1+mag/100: moon -> `t^2` (+20% by mid-event vs +10% before), pump/dump -> `t^1.5` (+/-21% by mid-event vs +/-16%), accumulate/distribute now drift to +/-25% of mag during the 0-70% phase (with light edge-tapered noise on top so the line still wiggles) before accelerating the remaining 75% of the move into the breakout/breakdown final 30%. + +### Changes +- **All DM notifications default OFF -- opt-in only**: Every `dm_*` toggle on `user_prefs` is now `DEFAULT FALSE`, matching the behaviour the four feed-style ones (`dm_nft` / `dm_predictions` / `dm_events` / `dm_ape`) already shipped with. Migration `0208_notify_default_off.sql` flips the column defaults forward AND rewrites every existing row to `FALSE` so already-registered players stop receiving DMs immediately on this deploy without anyone having to log in and run `,notify off` themselves. Players opt back in the same way they always could -- `,notify on` -- and the affected categories are: mining, transfer, validator, staking, itemlevelup, whale_alerts, 2fa, autolevelup, plus the four feed kinds. `,notify` (no args) now footers with `All notifications are off by default -- opt in with ,notify on`. Sender-side gates (`prefs.get("dm_validator", 1)` etc. in `cogs/trades.py`, `cogs/shop.py`) now default the missing-column fallback to `0` to match: an unknown column is treated as opt-in-not-yet-given. + +### New Features +- **Auto-rotating themed (season + 5 challenges) pairs**: Admins can now flip a single toggle and the bot will keep a coherent themed event running indefinitely without anyone having to babysit it. `,season auto on` enables rotation; the bot starts the next pair from `seasons_pairs_config.PAIRS` (Buddy Brawls / Trading Frenzy / Mining Madness / Fishing Frenzy / Yield Season / Risk Takers / Wild Season -- 7 pairs out of the box). Each pair ships **one season** (with its matching `seasonpass_config.THEMES` multiplier set + appropriate metric, e.g. `buddy_wins` for Buddy Brawls, `volume` for Trading Frenzy) **plus 5 themed challenges** (e.g. Buddy Brawls -> "Battle Royale" 500 buddy wins + "Adopt the Pack" 100 captures + "Arena Spawns" 200 + "Arena Champions" 150 + "Wild Captures" 50). Rotation triggers on **both** schedules per the request: when the active season's `ends_at` passes (default 7 days, configurable 1-30) AND when every paired challenge has settled (succeed or fail) the next pair starts immediately -- whichever fires first. Per-guild config: `,season auto pool ` (per-season prize pool, default **$10,000,000**), `,season auto cpool ` (total challenge pool, split across the 5 challenges by `pool_weight`, default **$5,000,000**), `,season auto days <1-30>` (per-pair duration), `,season auto next` (force-start the next pair manually after `,season end`). Pool ceiling raised to $100M so admins can scale further if needed. Bare `,season auto` shows the cursor + the full pair rotation list with the `->` marker on the next pair due. Hooks attach in `cogs/seasons.cog_load` to the existing `season_ended` / `challenge_succeeded` / `challenge_failed` bus events; the existing 5-minute season-expiry loop also runs `ensure_running` so a guild that flipped the toggle on after a restart picks up the next pair on the next tick. Pair tagging via `[auto:]` on each challenge description lets the completion-schedule path identify the cohort without changing the schema. Migration `0207_auto_seasons_pairs.sql` adds the `auto_seasons_enabled` / `auto_seasons_days` / `auto_seasons_pool_usd` / `auto_seasons_challenge_pool_usd` / `auto_seasons_pair_idx` columns on `guild_settings`. + +### Bug Fixes +- **`,inventory levelup vault` no longer crashes with `column "id" does not exist`**: The USD-priced wallet debit in `inventory_levelup` ran a hand-written `UPDATE users SET wallet ... WHERE id=$2` query, but the `users` table is keyed on `(user_id, guild_id)` -- so every Vaultstone level-up (the only stone with `accepted_currencies=("USD",)`) failed at the SQL boundary and the player saw the generic post-confirm error. Switched to the canonical `db.update_wallet(uid, gid, -cost)` helper which uses the right column and surfaces a real "Insufficient wallet balance" message when the player's wallet is short. +- **Themed stones no longer "level outrageously fast" after a single pay-out**: `services/themed_stones._grant` was the central XP-grant for Tidestone / Heartstone / Cryptstone / Bloodstone / Bloomstone / Gavelstone / Anvilstone / Chimerastone, and it skipped the `cap_xp(...)` clamp the legacy stones (Hashstone / Lockstone / Vaultstone / Liqstone) apply on every grant. Without the clamp, plant / harvest / cast / chat / battle / craft / swap / AH ticks banked unbounded XP past the next-level threshold, so a single auto-levelup pay-out instantly rolled the stone forward many levels (the player just had to wait for the 5-min poller and they'd see Bloomstone or Heartstone spike from Lv 3 to Lv 12 in one cycle). XP now caps at the next-level threshold per grant, matching the legacy stones, so each level requires a fresh stake-currency payment. +- **Stone shop / inventory / `,bal` now show the right pay-in currency**: The "Not owned" stub on `,inventory` and `,bal` hard-coded the cost as `XXXX DSD (any stablecoin)` for every stone, even though `accepted_currencies` was tightened months ago to `BTC/SUN`, `REEL`, `BBT`, `HRV`, `RUNE`, `BUD`, `FORGE`, etc. Players saw "buy for 7,500 DSD" but `,shop buy hashstone` (correctly) demanded BTC. The stub now reads the stone's actual `accepted_currencies` and surfaces the USD target alongside the network token list (e.g. `**$7,500.00** -- pay in `BTC` / `SUN``). The owned stat-row also falls back to the canonical first accepted currency when `lp_currency` was stamped on a legacy row that pre-dates the tightening. +- **Stone level-up cost no longer charges ~0 for non-stable stones**: `inventory_levelup` and the auto-levelup poller treated the result of `_levelup_cost(...)` as USD-raw, then divided by the live oracle to "convert" it to the target token -- but for a Hashstone / Tidestone / Bloomstone / etc. the staked-amount column already lives in the stone's stake currency (BTC / REEL / HRV / ...), so the result was already token-raw. The double-conversion produced a charge ~oracle-price smaller than intended (e.g. a Hashstone that should pay 0.025 BTC per level was paying ~8e-7 BTC), which combined with the cap_xp bug above let a freshly-bought stone vault through 5+ levels for pennies. The cost is now treated as same-currency by default; cross-currency level-ups (paying a BTC Hashstone with SUN) route through the stake-currency oracle -> USD -> target-currency oracle path so the dollar value of each level-up matches across all accepted currencies. +- **`,inventory` now lists every stone (incl. Gavel / Anvil / Chimera)**: The legacy inventory body had per-stone if-branches for the original 9 stones and silently dropped the three meta-economy additions, so `,inv` showed nothing for them even after purchase. The render path now iterates `_STONE_CFGS` so adding a new stone is a config-only change and `,inv` stays in lockstep with the shop catalog automatically. The "Not owned" / "Owned Lv X / Y" body shares one helper with the Hashstone-style legacy display. + +### Changes +- **Stone displays now show the staked balance in USD alongside the token amount**: `,inv`, `,bal`'s items page, and `,stake mine` (a.k.a. `,mystakes`) each pre-fetch a `{symbol: usd_price}` map once at command start and append `(≈ $1,234.56)` after the staked / earned / daily-est line in every stone or validator row. Dollar-pegged stones (DSD / USDC / USD-priced gavel-/chimera-/vaultstone) keep the 1:1 conversion implicit; non-stable stones (BTC Hashstone, REEL Tidestone, HRV Bloomstone, BBT Bloodstone, FORGE Anvilstone, RUNE Cryptstone, BUD Heartstone) read the live oracle so a player can see the dollar value of their position without hopping over to `,prices`. Lunar Mint positions on `,stake mine` also surface USD on staked group-token, session MOON, and lifetime MOON lines. + +### New Features +- **`,group hall prefix` -- group founders can drop the bot prefix inside the Hall thread**: New founder-only toggle that controls whether the Hall thread requires the bot prefix on commands. `,group hall prefix off` lets members type bare `work`, `fish`, `daily`, `swap`, etc. inside the Hall (same treatment admin-set bot channels already get); `,group hall prefix on` reverts to the default and re-requires the prefix; bare `,group hall prefix` shows the current state. Prefixed form (`,work`) keeps working in both modes. Stored on `mining_groups.hall_prefixless` (migration `0206_group_hall_prefixless.sql`); `core/framework/bot.py:_get_prefix` reads it on every message so the change is live immediately. Aliases: `prefixless` / `bare` / `noprefix`. +- **Three new meta-economy stones (Gavelstone / Anvilstone / Chimerastone)**: USD-priced leveled gems that scale off cross-cutting bot systems instead of a single minigame. **Gavelstone** (auction house) gains XP from each `,ah` buy and from each settled listing of yours (NOT from creating a listing -- spam-listing doesn't farm); pays buyer rebates on every purchase and seller bonuses on every settled sale, both surfaced on the receipt + DMs. **Anvilstone** (crafting) gains XP per `,craft` action regardless of qty; adds extra crafted units on top of the recipe's base output (free output, no extra input cost). **Chimerastone** (AMM swaps) gains XP per `,swap` (not bare `,buy` / `,sell`); reduces swap fees on top of any Liqstone discount. All three carry the standard `work_daily_bonus` per level so `,work` and `,daily` benefit too. Cost + level-ups are pure USD off the bare wallet, so the meta layer doesn't tilt toward any single network token. Drops into the existing shop / inventory / auto-levelup / DM / poller machinery via aliases (`,shop buy gavel`, `,shop buy anvil`, `,shop buy chimera`). +- **Two new expedition zones: Volcano and Void**: `,expedition` now offers the Volcano (min level 15, ore-heavy) and Void (min level 20, rune-heavy) zones for high-level buddies. New species affinities (salamander/ignis/molten for Volcano; voidling/nullfox/eclipse for Void), favoured crops, fish, and ore are included. Both zones have 8 zone-specific event templates for flavour. +- **24-hour expedition duration**: A new "24 hours" option (40 draws, +1400 XP, -20 happiness) is available in the expedition picker for players willing to commit to a full-day run. +- **Six new crops (seeds only)**: pepper, mushroom, lavender, rose, crystalmint, and dreamroot added to the farm seed shop. These can only be planted from seed packets -- no direct plant purchase. +- **28 new crafting recipes**: Includes fishing buffs (Angler's Paste, Siren Bait Brew), farming buffs (Harvest Tonic, Growth Serum, World Sap Distill), expedition buffs (Expedition Ration, Scout's Brew, Void Draught), buddy stat buffs (Buddy Energizer, Rose Petal Tea, Dreamroot Elixir), dungeon consumables (Vigor Brew, Pepper Salve, Dreamroot Ward), and eight buddy gear crafting recipes. +- **Buddy gear system**: Buddies now have two equipment slots -- `accessory` (cosmetic badge) and `charm` (passive stat bonus). Nine gear items available (4 accessories, 5 charms). Gear is visible in the buddy panel and in `,buddy gear`. Commands: `,buddy gear` to inspect, `,buddy gear equip ` to equip, `,buddy gear unequip ` to remove. +- **Cosmetic consumables**: Three new role-granting shop items -- Glamour Kit (500 DSD), Night Crystal (1,500 DSD), and Aurora Pass (5,000 DSD). Buy with `,shop buy ` and activate with `,inventory use ` to receive a Discord role. Use again to remove the role. +- **Crab trap dynamic panel**: The `,fish trap` command now shows a live ASCII frame that changes based on trap state (empty / soaking / ready), with interactive Place and Collect buttons. +- **Daily changelog auto-post**: Admins can configure a channel with `,admin setchannel changelog #channel`. The bot will automatically post new changelog entries once per day when the date advances. +- **`,drs fish @user` and `,drs farm @user`**: New DRS subcommands that pull full fishing/farming profiles (level, XP, rod/plot tier, biggest catch/harvest, payout totals, last active). Results DMed for privacy. Actions logged to the DRS audit trail. +- **`,admin cosmetic list/set/clear`**: Admins can now map each cosmetic item to any existing Discord role in the server, overriding the default name from config. `,admin cosmetic set glamour_kit VIP Glam` points the Glamour Kit at the "VIP Glam" role; `,admin cosmetic clear glamour_kit` resets it to default. +- **Sell back old gear at 50% price**: Weapons and armor in delves can now be sold with `,delve sell ` for 50% of their RUNE buy price (item must be unequipped). Fishing rods can be sold (downgraded) with `,fish sell rod` for 50% of the rod's REEL price. Crab traps sell at 50% via `,fish sell [qty]` (e.g. `,fish sell wire 3`). + +### Changes +- **2-round cooldown on healing potions + attack scrolls in `,delve`**: `,delve use ` (and the in-combat Potion button) now stamps a `potion_cd` buff with `duration: 2` whenever a `kind: heal` consumable is used; `,delve use ` does the same with `scroll_cd` for `kind: damage` consumables. `_tick_player_buffs` decrements both each combat action (attack / skill / flee), so the player has to take 2 swings before they can chain another heal or scroll. Surfaces a friendly `Potions are on cooldown for **N** more rounds` / `Attack scrolls are on cooldown ...` error on the in-channel button + the prefix command. Cooldowns are independent (heal CD doesn't block scrolls and vice versa). + +### Bug Fixes +- **`,ah browse` 6000-char overflow guard now uses the real embed length**: The previous fix tracked a manual char budget that decremented only at field-commit time, so an in-progress `current` buffer wasn't subtracted -- on busy guilds with long buddy rows the embed could still ship over Discord's 6000-char-per-embed cap and 400. Switched to `len(builder._embed)` (discord.py's `Embed.__len__` already sums every field name + value + title + description + footer for us) and refactored the categorised + single-kind paths to project the new size before adding each line / field; on overflow we commit whatever fits, drop the rest, and surface a `Truncated: +N more on this page didn't fit` hint pointing at kind filters / sort. +- **`,buddy pools` now shows the green `🟢` LP-marker**: The buddy-network swap-pair list (BUD <-> REEL / RUNE / MOON / FREN / HRV / BBT / INGOT) didn't surface which pairs the player was earning burn-fee rewards on, even though `_distribute_burn_lp_reward` (services/buddy_economy.py) pays a USD slice to LP holders of any pool containing the burned token. The list now mirrors `,trade pool list` -- pairs where the player has LP earning fees get a `🟢{aggregate_share_pct}%` tag, summing the player's share across every non-vault pool containing the partner token. Footer surfaces the legend. +- **`,ah browse` 6000-char embed overflow**: The browse embed packed every visible row into category fields without measuring the running per-embed character budget; busy guilds with metadata-heavy buddy listings could push the combined size past Discord's 6000-char-per-embed cap and get a 400 from `,ah browse`. The renderer now tracks a running budget against a 5750-char ceiling (6000 minus title + footer reserve) and stops adding rows once the next row would cross it, surfacing a one-line `Truncated: +N more on this page didn't fit. Filter by kind (...) or sort cheapest to narrow down.` hint so players know how to drill in. +- **`,expedition send` to Volcano / Void zones crashed with `CheckViolationError`**: The two new zones were added to `expeditions_config.DESTINATIONS` in the May-3 expansion but the `buddy_expeditions_destination_chk` constraint still only allowed the original four (forest / reef / mine / ruins), so any `,expedition send` for a Volcano or Void run failed on insert. Migration `0205_buddy_expeditions_volcano_void.sql` widens the CHECK to include both new keys. +- **Cosmetics overhauled to craft-only with time-limited 1h role grants**: `,shop buy ` no longer works -- glamour_kit / night_crystal / aurora_pass are now exclusively craftable via the new recipes `shimmer_dust` / `moon_essence` / `aurora_prism` (alchemy + enchanting specialties). Using a crafted cosmetic via `,inventory use ` no longer toggles the role on/off; instead the linked Discord role is granted for the cosmetic's `duration_seconds` (default 3600 / 1 hour) and a row in the new `cosmetic_role_grants` table tracks the deadline. A 60s sweeper in the Shop cog revokes expired roles automatically. Re-using the same cosmetic before expiry refreshes the deadline rather than removing the role. Migration `0204_cosmetic_role_grants.sql` adds the table. +- **Anvilstone repriced in FORGE (was USD)**: Anvilstone is the crafting meta-stone, so it now stakes in FORGE (the Forge Network coin) to match how Cryptstone takes RUNE, Tidestone takes REEL, etc., instead of USD. `cost_stable` stays as the USD-equivalent target; the buy + level-up flows convert to FORGE at the live oracle. `,inventory levelup` was extended to handle arbitrary non-stable tokens for any stone whose `accepted_currencies` lists them -- it defaults to the stone's stored `lp_currency`, validates the requested currency against the stone's accepted set, and routes the debit / treasury / vault calls through the right network. +- **Seed packets dropped off the `,farm shop` embed**: The shop embed only listed Plot upgrade + Fertilizer fields; seeds had to be discovered through the footer hint pointing at `,farm crops`. Seed Packets are now their own field on the shop embed, grouped by rarity (Common / Uncommon / Rare / Epic / Legendary), with the per-packet HRV price (the same `crop.hrv_sell_price * 0.20` formula `buy_seed_packet` uses) on every line. Chunks into `(cont.)` fields to stay under Discord's 1024-char-per-field cap as the crop catalog grows. +- **`,fish sell` only sold rods, not crab pots**: The sell subcommand could already settle individual trap keys (`,fish sell wire 3`) but had no bulk path -- a player with eight kinds of pots had to type eight separate sells. Added `,fish sell traps` (plural) which clears the entire crab-trap inventory in one call and pays back the combined 50%-REEL refund with a per-trap breakdown, plus `,fish sell all` to sell the full stack of one type. The unknown-target hint and the Tackle-Shop footer both surface the new options so players can find them without spelunking. +- **`,admin cosmetic list` "dictionary update sequence element #0 has length 1; 2 is required"**: `get_cosmetic_role_overrides` returned `dict(row.get("cosmetic_role_overrides") or {})`, but asyncpg hands back JSONB columns as a `str` (raw JSON text) unless a per-connection codec is registered. As soon as a guild had at least one override stamped in, `dict("...")` iterated the string char-by-char and blew up on the first non-pair character. The helper now parses string JSONB through `json.loads` and falls back gracefully on a parse error, so `,admin cosmetic list` (and the shop's role lookup) work regardless of how asyncpg returns the column. +- **`,shop` 6000-char embed overflow**: With nine stones rendered the items page already sat just under Discord's 6000-char-per-embed cap; adding a tenth stone tipped it over and `,shop` started 400ing with `Embed size exceeds maximum size of 6000`. The per-stone field body was rebuilt in compact form (cost + accepted + bonuses-per-level + XP source + owned line, ~150 chars) -- the verbose flavor description still lives on `,shop buy ` and on `,inventory ` so nothing was lost. Now fits 12+ stones with breathing room. +- **`,buddy gear` column error**: The gear equip/unequip/inspect queries used `active=TRUE` instead of the correct `is_active=TRUE` column name, causing a Postgres error on every gear command. +- **`,buddy` panel crash with gear equipped**: `gear` JSONB column is returned as a raw string by asyncpg; `gear_display()` was calling `.get()` on a string, causing `'str' object has no attribute 'get'`. The panel and all gear commands now coerce the value through `_as_dict()` before use. + +### Bug Fixes +- **`,ah buy` no longer crashes with `TypeError: '<=' not supported`**: The expiry check in `buy_listing` was comparing an epoch-float timestamp (from the DB) against a Python `datetime` object, which Python rejects. The check now runs DB-side via `EXTRACT(EPOCH FROM (NOW() - expires_at))` so the clock comparison is always between two numbers and container/DB skew is avoided. +- **Buddy level + XP no longer disagree across panels, expeditions, and combat**: Wild captures used to write `level=opponent_level` with `xp=0`, so the buddy panel rendered Lv. 1 while combat embeds happily reported the captured rank. The expedition / battle / craft XP grants then bumped `xp` without recomputing `level`, so a level-4 buddy (by XP) would still get gated out of a min-level-3 expedition zone because the column lagged. Both directions now stay in sync: the level column updates whenever XP changes, captures persist `xp = xp_for_level(level)`, expedition / battle / craft / chat XP paths recompute level inline, and migration `0198_cc_buddies_level_xp_sync.sql` backfills every existing row. Player-visible effect: the level shown on `,buddy`, on `,expedition`, on the upgrade panel, and during a fight all match. +- **`,buddy upgrade` shows the right unspent-points count after a level-up**: The upgrade panel and the spend SQL both used the stale `cc_buddies.level` column, so a buddy that just dinged on an expedition would still see "0/9 earned" until the next chat-XP tick. Both reads (the summary and the SQL cap check) now derive level from XP via `level_from_xp(xp)`, so the new point is spendable the second the level-up announcement fires. +- **`,delve upgrade ` no longer dies on a literal placeholder**: The command used to be `(hardiness, power, vigor, wisdom)` positional ints, so a player who copied `` from the help text or typed a stat name got a discord.py `BadArgument` and a generic error. The command now accepts mixed positional + named args (`,delve upgrade 2 1 0 0`, `,delve upgrade atk 5`, `,delve upgrade hp 3 atk 2`), warns specifically when it sees a ``, and the no-args panel ships a quick-spend button row so a player can just tap Hardiness / Power / Vigor / Wisdom to spend one point at a time. +- **`,shop buy specialty_slot` no longer reports "Purchase failed -- try again."**: The wallet debit query used `WHERE id = $2` against the `users` table, which keys on `(user_id, guild_id)` -- the column name was wrong, so every purchase raised on the SQL and the user got the generic catch-all error after confirming. Switched the WHERE to `user_id = $2` so the third-slot unlock actually charges and lands. +- **`,farm buy ` (and bare crop names) now route to the right category**: The farm-shop quick-buy modal forwards the raw item name into `,farm buy ...`, but the command required ` [qty]`. Typing `,farm buy Bonemeal` used to fall through to the unknown-category error. The buy parser now resolves a bare first token against the fertilizer table and the crop table before complaining, so `Bonemeal` or `wheat 5` work without remembering `fertilizer` / `seed` prefixes. +- **`,buddy storage` now spells out gender and rarity**: The display already had the gender glyph and tier name, but they read as raw symbols (`♂`, `Rare`) tucked into a single line. Each storage row now has its own subtitle line spelling out **Rarity:** and **Gender:** in plain words alongside the glyph, so a player browsing PC-stash can read the buddy's lineage without decoding the icon language. +- **Delve wild-buddy captures tell you when the shelter is full**: When the player's owned-buddy count was at the cap, the delve wild-buddy capture path silently dropped the cc_buddies INSERT but still rendered "Captured!" -- so the player saw a success message and then couldn't find the buddy anywhere in `,buddy`. The verdict footer now spells out that the buddy joined the delve party only and asks the player to free a shelter slot to keep future captures. +- **`,ah` 400 on field overflow**: The categorised browse view (with no `kind` filter) packed every listing of one kind into a single embed field; once a guild had more than ~10 active fish or buddy listings, the joined string blew past Discord's 1024-char-per-field cap and the whole embed 400'd. Field rows now chunk into successive `Kind (cont)` fields kept under 1000 chars, mirroring the wrap pattern `,buddy pools` and `,buddy species` already use. +- **Delve mine button doesn't reply with what was mined**: A bus-listener exception in `_fan_out` was swallowing the followup that announced the mining receipt -- the player saw the room view rebuild but no Pickaxe Strike message. The receipt is now sent BEFORE the bus fan-out (so a misbehaving listener can't suppress it) and posted publicly instead of ephemeral; bus events run after with a try/except so a single bad listener never bricks the action again. +- **Delve Mine / Open buttons silently no-op when no junk drops**: Both handlers passed `view=drop_view` (where `drop_view` was `None` for the common no-junk case) into `interaction.followup.send`. discord.py 2.3's webhook send raises `TypeError` on `view=None` because the hasattr check trips on NoneType, and the surrounding `except discord.HTTPException` doesn't catch it -- so the receipt never posted and `_rebuild_room_message` never ran, leaving the room embed frozen on the same vein / chest. The next click then hit "No ore vein in this room." against the now-stale embed. Both buttons now only pass `view=` when a drop view actually exists, so the receipt fires and the room embed advances on every click. +- **Delve Pray button missing on shrine rooms**: The shrine room embed advertised the boon roll, but the room view was missing a `Pray` button -- players could only activate the shrine via the `,delve pray` slash command, which is awkward when the room view is right there. Pray now appears next to Open / Mine / Descend on row 0 whenever the current room type is `shrine`, executes the same boon-roll path, and posts the blessing / curse receipt on the same row. +- **Delve sell-all junk / mats / all leaves stale buttons**: The bulk-sell flow on the junk inventory panel ran the sell correctly but never refreshed the panel embed or buttons. Players reported it as "doesn't work" because the second click hit an empty inventory and replied with "Junk inventory is empty." The panel now rebuilds in place after every bulk sell so the items the player just sold disappear from the view. + +### Changes +- **`,ah` browse + search collapse identical listings from the same seller**: A seller posting 50 Healing Herbs at the same price (or 8 buddies sharing name / level / rarity / gender) used to take 50 separate rows, drowning out the rest of the market. The browse and search views now fold rows that share seller + kind + ref + price + currency + visible identity (rarity / level / name / gender / species) into one line whose qty is the sum across the stack and whose footer reads `×N listings (buy # first)`. The lowest listing id stays on the line so `,ah buy` works straight from the embed. +- **Leaderboards now exclude Disco, banned / left members, and `User 0`**: `,lb` and every delegated leaderboard (`,lb buddy`, `,lb battles`, `,lb arena`, `,lb delve`, `,lb fish`, `,lb biggest`, `,lb farm`, `,lb craft`, `,lb achievements`, `,lb level`) now drop `user_id == 0` placeholder rows, the bot's own row, any other bots in the guild, and members no longer in the guild (left or banned). New `core/framework/leaderboard.filter_lb_user_ids` helper centralises the filter so all 15+ LB call sites stay in sync; `Bank._lb_resolve_and_filter` is the cog-side wrapper that applies it before slicing the top-N. +- **`MAX_HELD_EGGS` lowered from 50 to 10**: held eggs are now strictly the "with you" tier; the 50-and-up capacity moved to the new banked egg storage. Existing players with more than 10 held eggs retain them on read; new captures route to banked storage once the 10-cap is reached. +- **Capture refusal messages updated**: shelter-full errors across fishing / farming / dungeon / market / world surfaces now say "battle-active buddies" and point at `,buddy slot battle buy` (or `,buddy store` to free a slot). + +### New Features +- **`,expedition send` lets you pick which buddy to send**: The picker view now ships a buddy dropdown (active-first, capped at 25 entries) so a player can deploy any owned buddy that isn't already on a run, not just whichever one happens to be active. The selected buddy renders inline on the picker embed; the service layer accepts an optional `buddy_id` and falls back to the active buddy when none is supplied so legacy callers stay working. +- **`,buddy storage` is now a button-driven panel**: The storage embed used to be a static text list with a footer telling the player to type `,buddy retrieve `. It now ships a Withdraw / Deposit / Eggs / Refresh button row -- Withdraw flips a dropdown of stored buddies (one click to pull one back), Deposit flips to an owned-buddies dropdown (one click to stash), and Eggs hands the player to the existing `,fish egg` picker which already has hatch / sell / gift / list controls. Held eggs aren't cc_buddies rows so they intentionally route through the egg picker rather than mixing into the same select. +- **Full-slot warnings on game-start + fight-start**: A new `core.framework.slot_warning.maybe_warn_full_slots` helper checks the player's owned-buddy shelter cap and held-egg cap, and posts an inline warning when either is full. Wired into `,fish` / `,delve` / `,farm` (game start) and into the wild-buddy battle entries on all three surfaces (fight start). Per-(uid, gid, surface, phase) 10-minute dedupe keeps the notice from spamming on every cast / room. Player-visible effect: a player with a full shelter knows up-front that a winning capture won't land before they bother fighting, instead of finding out after the fact. +- **Battle slots vs. storage slots split + buddy egg storage**: The single 3-slot ownership cap (extendable to +100 via `,buddy slot buy`) is replaced with three independent purchasable capacities. **Battle slots** = active buddies (`status='owned'`); base 3, max 10, +1 per upgrade at `,buddy slot battle buy` (25,000 BUD each). **Storage slots** = stored buddies (`status='stored'`); base 10, max 100, +10 per upgrade at `,buddy slot storage buy` (5,000 BUD each). **Egg storage** = the new banked-egg container on `user_buddy_economy.egg_storage`; base 50, max 1000, +50 per upgrade at `,buddy slot eggs buy` (2,500 BUD each). Captures (escape events, fishing/farming/dungeon wild-battle wins) auto-route into a battle slot when one is open, else fall through into storage when there's room, else refuse cleanly. Existing `bud_slots_purchased` from the old single-cap system is migrated forward as storage upgrades (clamped to the new cap) so no purchase is lost. +- **`,buddy storage eggs` -- consolidated egg panel**: Held eggs (the on-person fishing inventory; capped at 10, not upgradable) and banked eggs (the upgradable buddy storage container) now render side-by-side under one panel with `,buddy storage eggs`. Companion `,buddy egg deposit [n] [species]` and `,buddy egg withdraw [n] [species]` commands move eggs between the two surfaces (FIFO selection, optional species filter). Wild-battle / fishing eggs that overflow the 10-cap held container automatically spill into banked storage; only when both are full does the egg fall back to the legacy LURE mystery-box payout. +- **Buddy shop now lists item id + name + price for every upgrade**: Shop embed renders each purchasable as `id name - price (BUD)` (the literal id is what the Quick Buy modal accepts), with the slot progression detail on the line below. Four items: `slot battle`, `slot storage`, `slot eggs`, `attractor`. + +## [main] -- 2026-05-02 + +### New Features +- **Delve junk drops: salvage / craft mats / usables**: Combat wins, chest opens, and ore mining now have a depth-scaled chance to drop a secondary item alongside the primary loot, mirroring fishing's junk system. Sixteen items across three kinds: salvage trash (broken blade, torn cloth, rusted buckle, cracked shield, tarnished coin, moldy tome, skull token) sells straight to RUNE; craft mats (monster fang, ectoplasm, bone fragment, glowing crystal, dragon scale) bank for future crafting; and four usables (Healing Herb / Smoke Bomb / Lucky Charm / Scrap Arrow Bundle) can be consumed mid-run for in-combat effects via `,delve junk use `. Mining drops at 40% the combat rate; chest drops independently of the relic roll. New `user_dungeon.junk_inventory` JSONB + `total_junk_collected` counter; `,delve junk` lists owned items grouped by kind, `,delve junk sell ` cashes them out for RUNE. +- **Junk drops have inline buttons in the delve game**: When a chest, mine, or mob kill rolls a junk drop, the receipt embed ships an ephemeral Use / Sell / Bag quick-action view -- Use only renders for usables (Healing Herb, Smoke Bomb, Lucky Charm, Scrap Arrows), Sell pops just that one drop for RUNE, Bag opens the full inventory panel. The room view itself now has a permanent **Junk** button on row 2 that opens an inventory panel with Use buttons for each owned usable plus Sell Salvage / Sell Mats / Sell All buttons (and a Refresh on the bottom row alone, per the project-wide convention). No more typing `,delve junk use healing_herb` mid-combat. +- **Farming harvest now shows rarity + can return seed packets**: Single-plot and harvest-all replies tag every crop with its rarity (`Common` / `Uncommon` / `Rare` / `Epic` / `Legendary`) so the player sees what their RNG actually rolled. On top, every harvest has a rarity-scaled chance for the crop to "go to seed" and drop 1-3 packets of itself back into your seed_packets bag (Common: 30% / 1-3 packets, Legendary: 7% / 1 packet). Lets a lucky harvest restock your planting supply without a shop trip. + +### Changes +- **Stake panels are now identical across every minigame**: `,fish stake`, `,buddy stake`, and `,delve stake` now render the exact same Stake / Unstake / Claim / Refresh button panel `,farm stake` and `,craft stake` already use. Multi-token panels (Buddy: FREN+BBT, Delve: COPPER+SILVER+GOLD) drive the same buttons via a token field on the amount modal so a player learns the surface once and uses it everywhere. +- **Stake / unstake / claim receipts unified across the codebase**: Every `,x stake ` and `,x unstake ` reply now renders through `core.framework.staking.stake_receipt` (lock/unlock title + delta + total + USD on every line + yield-paid block when an unstake auto-claims). Every `,x claim` reply renders through `claim_receipt` (yield paid + remaining stake + USD), and every `,x cashout ` renders through `cashout_receipt` (gross USD + net credited + oracle before/after + slippage + LP-fee). Receipts ship with crypto and USD on every line in the same order regardless of which game emitted them. +- **Delve room buttons collapsed onto one action row**: `,delve next` previously stacked Rest on row 1 and the consumables dropdown on row 2 (each its own row), so a corridor room ate four vertical rows for what was effectively five buttons. Rest now sits on row 0 alongside Next / Mine / Open / Descend, and the consumables dropdown only renders when the player actually owns a consumable -- empty rooms now show one action row + the lone Bump on the bottom row. + +### Bug Fixes +- **`,buddy pools` no longer 400s on Discord's 1024-char field cap**: All swap-pair rows were joined into a single `Pairs` field which overflowed the moment 5+ pairs had live oracles, surfacing as `400 Bad Request: Invalid Form Body / In embeds.0.fields.0.value`. Rows now pack into successive `Pairs (cont)` fields, each kept under 1000 chars, mirroring the chunking strategy `,buddy species` already uses. + +### Changes +- **INGOT promoted to a bidirectional buddy swap partner**: INGOT used to live in `Config.BUD_ONEWAY_IN_TOKENS` so it could only burn INTO BUD, never out. The original concern (USD-whale exposure on INGOT supply) is already covered by the broader EARN_ONLY firewall: BUD itself is earn-only with no `.buy` / `.swap` path, so a USD whale has no way to acquire BUD without first earning into the closed loop, regardless of which way the BUD<->INGOT pair points. INGOT is now in `BUD_SWAPPABLE_TOKENS`, the one-way set is reserved empty for future carve-outs, and `,buddy convert` / `,buddy quote` / `,buddy pools` / `,trade pool list buddy` all render INGOT as a normal bidirectional pair. +- **`,trade pool list` now includes the Buddy Network**: Buddy-network burn-swaps (BUD <-> FREN/REEL/RUNE/MOON/HRV/BBT/INGOT, all bidirectional) appear as a synthetic "Buddy Network" entry in the existing network dropdown, rendering depth (oracle x circulating supply) per side, the spot rate, and per-direction sample-slippage. Buddy Network depth folds into the platform-wide TVL line so the global figure stays accurate. Reachable directly via `,trade pool list buddy` and through the dropdown. + +### New Features +- **`,buddy pools` overview**: One-shot panel listing every BUD swap pair (six bidirectional partners + INGOT one-way) with the spot rate, synthetic-pool depth on each side, and the slippage a sample-sized swap would feel in each direction. Pass a number to rescale the probe (e.g. `,buddy pools 1000` for a $1k probe). +- **`,buddy convert` slippage guard**: Optional 4th arg `max_slip` (percent) pre-quotes the swap and aborts if predicted slippage exceeds the cap. `,buddy convert reel bud 100 5` only fills if slippage stays under 5%; without the arg the swap fills as before. Backwards-compatible. +- **`,admin pump auto` controls + market-event announcements**: New subcommand surface on `,admin pump`: `status` shows scheduler state and next-fire countdown, `on`/`off` toggle the auto-pump for the current guild (in-memory; the global kill-switch is `Config.AUTO_PUMP_ENABLED`), and `now` forces an immediate roll. Each auto-pump fire also posts a public market-event embed to the guild's `events_channel` (falling back to `crypto_channel`, silent if neither is set), matching the announcement pattern used by rare achievements. +- **`,buddy quote` previews any buddy burn-swap**: New read-only command shows the spot vs. effective exchange rate, synthetic-pool depth (oracle x supply) on each side, per-side oracle price impact, headline slippage, the LP-reward fee, and the estimated output amount before the player commits. Same legal pairs as `,buddy convert` (BUD <-> FREN / REEL / RUNE / MOON / HRV / BBT bidirectional, plus INGOT -> BUD one-way). +- **`,buddy convert INGOT bud` opens INGOT's earn-loop exit to BUD**: New `Config.BUD_ONEWAY_IN_TOKENS = {INGOT}` carve-out lets crafters dump INGOT into BUD via the existing burn-swap (slippage on both oracles, LP fan-out). Direction is one-way -- BUD -> INGOT stays blocked so the EARN_ONLY firewall on INGOT supply still holds (only `,craft make` can mint INGOT). +- **Random hourly admin-style pumps on a random asset**: New `auto_pump_task` in `cogs/admin.py` fires every ~55-75 minutes per crypto-enabled guild, rolling a random non-stable / non-pegged token plus a random chart pattern, magnitude, and duration. Uses the same `_admin_price_events` slot as `,admin pump`, so the price-tick loop drives the chart through the rolled pattern; `.buy` / `.sell` / pool swaps continue to apply per-trade impact and slippage on top of the pumped oracle, so the firewall stays up. Tunable via `Config.AUTO_PUMP_ENABLED / AUTO_PUMP_INTERVAL_MIN_S / AUTO_PUMP_INTERVAL_MAX_S`. +- **`,today` / `,start` surface every new farm + delve loop**: Forage, contract turn-in, shrine pray, and chest open auto-appear as one-shot Success buttons on the home tab the moment they're actionable; the Farming tab adds Forage / Turn In / Contract buttons and renders today's contract progress, mutated-plot count, and forage cooldown; the Delve tab renders the equipped relic, owned-relic count, armed run curse, and a "Shrine awaiting" prompt when standing in one, with new Pray / Open / Relics / Curses action buttons. `HubSummary` grew `forage_ready`, `contract_actionable`, `delve_shrine_in_room`, `delve_chest_in_room` flags fed by DB-side probes. +- **Refresh / Bump bottom-row convention enforced**: Every Refresh and Bump button across the bot now lives ALONE on the bottom row of its View, never sharing a row with action buttons. Fixed seven violators -- `FarmFieldView` (`Refresh` + `Bump` were on rows 0/1), `_CastResultView` (Bump on row 0 with Panel), `_DuelsIssueView` + `_WildBuddyBattleView` (Bump on row 1 with combat actions), the buddy battle-result view (Bump on row 0), and both `_DelveRoomView` + `_DelveBattleView` (Bump on row 1 with Rest / Flee). All now sit on row 4 alone. +- **Farm Forage minigame (`,farm forage`)**: New wander-the-brambles minigame mirroring `,fish dig` -- free roll on a 10-minute cooldown, weighted outcome on a small/big HRV purse, SEED pile, stash of seed packets, fertilizer pack, the Ancient Tuber jackpot (multiple legendary crops straight to your bag), or just brambles. Multi-frame ASCII reveal with a pre-frame "wandering..." beat that edits to the outcome frame. Lifetime forage count tracks on `total_forages`. +- **Delve Shrine Pray (`,delve pray`)**: Shrine rooms can finally be activated. Random boon weighted across full heal, RUNE jackpot, +40% ATK blessing (6 rounds), +20% SPD blessing, free relic from the deep-floor pool, or a Cracked Promise curse that costs 25% HP but doubles the next chest. Multi-frame ASCII (kneeling -> blessing or shrine bites back). Bumps `total_shrines_visited`. +- **Mutation Harvest reveal frame**: Mutated harvests now get their own dedicated burst-of-light ASCII embed instead of a one-line tag, so the moment lands. Single-plot harvest only -- harvest-all retains the compact summary. +- **Relic + Curse ASCII frames**: Cracking a chest that drops a relic now switches to a relic-glow frame; arming a run curse via `,delve curse set` shows the curse-armed frame with the per-curse multiplier breakdown. Same code-fenced inline ASCII pattern used by chests / mining / shrine. +- **Crop Mutations: Golden / Giant / Rainbow**: Every plant roll now has a small, rarity-scaled chance to mutate into one of three special variants -- Golden (2.5x SEED), Giant (3x harvest qty), or Rainbow (4x SEED + 2x qty). Mutations roll at plant time, show as a badge on the plot view all the way through growth, and apply their kicker at harvest. Bloomstone yield bonuses also nudge mutation odds so late-game plot stones feel a little more alive. +- **Daily Farming Contracts**: One rolling NPC contract per UTC day per player asking for a specific crop in a specific quantity. Reward scales with crop rarity (HRV + SEED, common pays ~50 HRV, legendary pays thousands). Mid-progress turn-ins are scaled so a player can split delivery across multiple harvests. View with `,farm contract`, deliver with `,farm contract turnin`. Lifetime completions track on `total_contracts_completed` for future leaderboards. +- **Delve Relics: 9 passive items, deep-floor chest drops**: Cracking chests on floor 5+ now has a depth-scaled chance (capped at 30% on the deepest floors) to cough up a relic. Owned via `,delve relic`, equipped one-at-a-time. Effects span Miner's Charm (+25% mining), Lucky Coin (+8% crit), Iron Heart (+15% max HP), Swiftboots (+0.06 SPD), Rune's Eye (+25% RUNE drops), Vampire Fang (8% lifesteal on kills), Arcane Focus (+30% spell damage / +5% crit), Thorn Aegis (briar reflect + extra HP), and the legendary Godslayer's Eye (multi-effect). Relics persist across runs. +- **Cursed Runs: opt-in run modifiers**: Arm a curse before `,delve start` for harder mobs and juicier drops. Bloodmoon (+30% mob dmg, +50% RUNE), Frenzy (+50% mob HP, +60% RUNE), Famine (no potions, +100% RUNE / +100% chests), and the legendary Abyssal Pact (mobs +50% HP and dmg, all rewards x2.5). Curses clear automatically on rest; lifetime completion counter on `total_curses_completed`. +- **Buddy arena: win streaks, daily boss, random modifiers**: Three layers added to `,buddy arena` so the loop has more to chase past the lifetime tier ladder. (1) Win streaks track consecutive arena wins (with a permanent personal-best); each consecutive win adds +5% to BUD/BBT/XP up to a +60% cap, and a loss resets the streak. Streak board on `,buddy arena streaks`. (2) Daily boss `,buddy arena boss` -- a once-per-24h fight against a +5-level legendary opponent with x1.6 HP / x1.25 ATK that pays 4x BUD + BBT plus a flat boss cherry. (3) Every arena fight now rolls a random modifier (High Tide, Glass Cannon, Bloodbath, Overclock, Chaos, Marksman, Low Gravity, or Standard) that mutates the live battle and grants up to +40% reward for the riskier ones. Streak / modifier / boss multipliers are surfaced on the intro embed, on every round, and broken out on the result so the math is visible. + +## [main] — 2026-05-02 + +### Frontend/UI +- **Holdings goes two-column + adds NFTs / Farming / Crafting**: The Summary tab's Holdings block was missing three NetWorthResult; components that already roll into the headline Net Worth figure --; nft_value, farming_*, crafting_*. A player with a NFT pile or a (`7ad58e70`) +- **Reform Summary tab -- clean, minimal, informative**: The default `,bal` landing card was a wall of up to 22 inline; `≈ $X,XXX.XX` fields with the headline (Net Worth + PnL) tucked at; the bottom inside two more inline fields. Reformed so: (`d1a90ede`) +- **Tighten footer hints (clean / minimal / informative)**: Drop decorative 💡 / ⚡ glyphs from informational footers (footer is; already secondary subtext, the emoji wasn't carrying meaning) and; collapse verbose pipe-separated command lists into terse comma lists. (`f7b4a2a2`) +- **Retire last raw discord.Embed() calls in framework layer**: Three high-traffic embed surfaces in core/framework/ were still bypassing; the canonical card() builder:; - core/framework/bot.py: fuzzy "did you mean?" suggestion (every typo), (`bde33630`) +- **Button layout polish + finish embed/glyph centralization**: Picks up where the last UI pass left off.; Button layout fixes (player feedback: "buttons are weirdly placed"):; - cogs/showcase.py (`,me`): Bump button used to sit alone on Row 1 (`b61a91bf`) +- **Tighten reply helpers, centralize rarity/section colors, add fmt_rel**: Three UI/UX consistency wins.; 1) Centralized rarity & system-section colors. Moved the per-cog `_RARITY_COLOR`; dicts out of cogs/farming.py and cogs/crafting.py into a single source of (`4e614438`) + +### Maintenance +- **Tidy project layout**: - Delete the empty diff.txt junk file at repo root.; - Eliminate the stale top-level migrations/ directory: the only SQL file; was a duplicate of database/migrations/0014_add_faucet_schema.sql, and (`35c9d0fd`) + +### Discord Bot +- **,today: rename Daycare -> Nest and stop double-listing stat points**: Two long-standing copy bugs on the unified Home / Today panel.; (1) The Buddy tab still labelled the breeding surface "Daycare"; everywhere visible to the player -- the Buddy-tab jump button, (`26ec942f`) +- **Quick Buy modal on every per-game shop, currency-locked**: ,shop already had a Quick Buy button + modal that re-dispatched a; synthetic ,shop buy through the bot's command pipeline. The; per-game shops (,farm shop, ,fish shop, ,delve shop, ,buddy shop) (`550dfd07`) +- **Fix ,shop Delves dropdown silently doing nothing**: The Delve Shop section packed 44 weapons / 25 armor / 29 consumables; into three fields of one embed. Every field blew past Discord's; 1024-char-per-field cap and the embed body itself broke the 6000-char (`4cd51833`) +- **Fix shop Quick Buy modal failing with "interaction failed"**: Two latent bugs in _ShopBuyModal that have been there since the; modal was wired:; 1. self.view = view shadowed discord.ui's internal view attribute (`08e07988`) +- **Surface AH listing syntax + last-sold price in receipts**: Players asked for crafting and the auction house to be linked. The; two commands you stare at after picking a recipe -- ,craft info and; ,craft make -- now both include the exact ,ah list (`7847089b`) +- **Message-id recovery for wild fishing captures**: Closes the "I caught a buddy in this message but can't find it"; report loop. ,buddy find resolved the player-side discoverability,; but admins handling reports still had no way to map a Discord (`f0caff97`) +- **Fix shop embed exceeding 6000 chars after restyle**: The items page lists 9 stones; injecting a 4-line wallet stat-row; field at the top + the new owned-stone stat-row line tipped the; embed over Discord's 6000 char-per-embed cap on accounts with (`56f218c9`) +- **Fix ,admin pump active 6000-char embed overflow**: Each active event was rendered as its own embed field. With many; concurrent events (e.g. after ,admin pump each:builtin schedules ~25; tokens at once) the rendered embed exceeded Discord's 6000-char and (`65fe35a9`) +- **Wild fishing: explicit "almost!" when capture refused at cap**: Follow-up to the buddy-storage thread. Wild-battle wins silently; swallowed the capture roll when the player was at their owned-buddy; cap -- the receipt showed only the LURE/REEL payout, no signal that (`6b9ebb08`) +- **Buddy storage: surface rarity, fix shelter mislabel, add ,buddy find**: Three player reports about buddy storage all trace to the same root: wild; captures from fishing land in cc_buddies as status='owned' is_active=FALSE,; but the success message tells players they "joined your shelter" -- so they (`c28b0403`) +- **Restyle ,today / ,start and ,shop panels with stat-row bars**: Match the polished result-card style players see in fishing /; casino popups: a tight status block of "count / max bar label"; rows at the top, italicised tagline, and (for shop) a thumbnail. (`d8073dbd`) +- **Expand admin pump into a chart-pattern engine with category targets**: `,admin pump` was previously a single straight-line price move on one; symbol or `everything`. Player feedback wanted variety, randomness, and; the ability to target specific slices of the token universe (network (`b884e312`) +- **Farm fertilize-all, welcome DM, first-time game intros, beefed delve stats**: ,farm fertilize all + button; - New apply_fertilizer_all service iterates every growing plot that; doesn't already have fertilizer, applies the equipped one, and (`4a761403`) +- **Fix ,farm seed display + ,delve stats redirect**: ,farm field view: the field labelled SEED only ever showed the SEED-; token wallet balance, which sits at ~0 for almost every player (SEED; is short-lived between harvest and swap). The actual seed inventory (`3a6285ce`) +- **Fix delve shop embed size: paginate categories at 8 items/page** (`cc88281a`) +- **Expedition lock: block wild battles + crafted treats when buddies are deployed** (`2b9908bc`) +- **Put REEL button on the same row as HOOK** (`947d1bc9`) +- **Fix today panel: hydrate quest name/icon from quests_config template** (`44d87deb`) +- **Fix expedition lock query: e.id -> e.expedition_id** (`21edecf1`) +- **Server calendar with mines-style grid + ,today inline feed + admin auto-post** (`5ef1798d`) +- **Game embeds skip autodelete + delve shop dropdown browser with clean item stats** (`fd6efe45`) +- **Unify ,today and ,start into one tabbed interactive panel** (`43603fcb`) +- **Wild-battle buddy XP visibility, expedition lock + payout bump, today panel running fix** (`902f30c3`) +- **Delve phase 4: today panel surfaces unspent player + buddy stat points** (`87337ea0`) +- **Delve phase 3: 11 class craftables, 6 quests, 9 achievements + class-suffixed bus events** (`5c17cba2`) +- **Delve phase 2: ranged combat, ammo, buffs, Volley/Wildshape, ,delve upgrade + reroll** (`facd3053`) +- **Delve phase 1: archer + druid classes, weapon/armor types, stat alloc + reroll schema** (`3145ea67`) +- **Buddy panel + shelter clarity: rarity in shelter, stat-point upgrades on the panel** (`e40cef05`) +- **Dynamic hook window + secondary REEL action** (`8802e023`) +- **,buddy species interactive roster with affinity filter**: Was a flat embed listing every species grouped by rarity. Now an; interactive panel:; - Row-0 dropdown filters by expeditions_config.SPECIES_AFFINITY (`a773793f`) +- **,items per-token picker + visible token IDs**: Player feedback: "make individual items in a stack selectable, so; if I want to use a particular egg or a particular fish I can send /; use / list it individually. Also need to add the token id to items (`64058e0b`) +- **Try Capture button on shelter-escape + fishing wild encounters**: Player asked for Capture to be available on every wild buddy; encounter. Audit showed delve and farm already had it; the two; remaining were: (`66f6a33b`) +- **Buddy panel dropdown stops desyncing on the live tick**: Player reported: "still broken with the drop-down, but not with; arrow keys." That last clue cracked it -- buttons survive but the; dropdown breaks. Buttons are static across re-renders; the dropdown (`35fe45d0`) +- **BuddyPanelView is persistent: clicks survive bot restarts**: Player-reported repeatedly: "this interaction failed" + log warning; "View interaction referencing unknown view for item <_BuddySelect>.; Discarding" on every dropdown click after a redeploy. Root cause: (`923b0212`) +- **,buddy species shows new rarity-extra lanes per-tier and per-row**: Player feedback: "where do I see the new buddy passives? They aren't; showing on buddy embeds or species list or anywhere." The ,buddy; panel got a ✨ Passive effects field in 05b51c2 but the ,buddy (`571c8888`) +- **Cc_buddies status check + ,items sort SQL + graceful shutdown**: Three issues from the latest prod logs:; 1. ,admin buddy spawn -> CheckViolationError on cc_buddies_status_chk; Migration 0169 (storage status) rewrote the CHECK as (`3dceabca`) +- **Sweep _refresh collisions + downgrade drs_commands diag warning**: Same discord.ui.View framework collision fixed earlier for _HubView; turned up in three more views. discord.py's internal sync; _refresh(components) gets called during component rehydration; any (`0afd4053`) +- **Cross-cog buddy passives + rarity-extra lanes + attractor wiring**: User asked for: more buddy passive effects (species + rarity), with; rarer buddies getting multiple effects across fishing / farming /; delves / crafting / arenas / battles, and to make them visible. Also (`05b51c2e`) +- **Prod-log bug fixes: FK on chat XP, hub _refresh collision, nest errors**: Three issues from the latest deploy logs + one player report:; 1. RedisBus chat_level_up handler trips fk_user_badges_user for users; who only chat (no users row yet). services/achievements.grant now (`4cb5a3ba`) +- **,today ready-feed accuracy pass**: Player feedback on the ,today panel:; - Trap hint fired for any placed trap, not just ready ones, and; pointed at ,fish trap (the parent group) instead of ,fish trap (`2b69cf6d`) +- **Buddy food rebalance + cooldown + unify daily claim with earn cog**: Two player-reported issues, one commit:; 1. Crafted food was free unbounded XP. Training Brew (+500) and; Harvest Pie (+1500) outpaced every other XP source; players (`258e8f5b`) +- **AH buddy / egg listings show rarity by NAME**: User reported: "Ah listed buddies don't show rarity :( nor on the; more info page". Two compounding issues:; 1. Active listings created before the create_listing_by_token (`7926a4ac`) +- **,shop buy no longer needs the currency arg**: User feedback: "ppl saying that they can't buy a bloomstone even; tho they have HRV" -- they had to type ,shop buy bloomstone HRV; because the cog defaulted currency="DSD" which the stone rejects. (`8e066483`) +- **Rename Daycare -> Nest + wire Shop Quick Buy modal**: User-facing rename only. The DB table cc_buddy_daycare and the bus; event daycare_egg_collected stay for schema / subscriber stability;; every label, embed title, error message, hint, and command name the (`b9f8d19e`) +- **Fix buddy slot cap + AH buddy escrow + listing freshness**: Three player-reported issues, one commit:; 1. Extra buddy slots are now honored across every capture / hatch /; adopt path. The canonical per-user cap helper was already there (`f2e6d091`) +- **,today panel: more ready probes + row-2 quick-collect buttons**: The Ready right now feed was stuck at 5 probes (eggs / daycare /; plots growing / AH active / expeditions). The hint copy was also; loose -- "X plots growing" fired for any planted plot, not just ripe (`3ab1aa0d`) +- **Wire ,expedition into hub / quests / achievements**: The expedition cog shipped standalone last commit. This commit binds; it to the existing engagement systems so a player who never opens; ,expedition still feels its impact and a player who does sees (`09fd9955`) +- **Add ,expedition -- AI Buddy Expeditions**: A new daily / multi-day engagement loop that binds the existing cogs; together without bolting on new mechanics. Players send their active; buddy on an autonomous run to one of four destinations (Whispering (`e056db83`) +- **Rename ,hub to ,today (collision with ,menu and earn ,daily)**: Deploy crashed on startup: ,hub is already an alias on cogs/overview's; ,menu (minigames hub) and ,daily is cogs/earn's 24h work check-in, so; discord.py refused to register the cog with CommandRegistrationError. (`c0dec05e`) +- **Add button-based egg + buddy selection (,fish egg picker, ,buddy select)**: ,fish egg now opens an interactive picker view: the status embed stays; the same but a new owner-locked view attaches a row-0 dropdown listing; every (species, rarity) bucket the player holds, and four action (`2a329b8f`) +- **Add ,hub daily front-door panel with login streak**: Discoin had every system shipping in parallel but no central place; for a player's daily routine. ,hub opens an owner-locked interactive; panel that stitches the existing systems together: (`f724bc7a`) +- **Per-item lexicon detail + ,items / ,db row reflow**: services/lexicon.py was a per-kind copy table that said the same thing; for every fish, every weapon, every recipe. It now loads the actual; catalog dict and surfaces real per-item detail: (`506261d2`) +- **Wire native catalog prices into ,items and ,db**: Most catalogs quote in their network token (REEL for bait, RUNE for; weapons / armor / consumables, FGD for crafted, HRV for crops); the; bootstrap was only filling the USD column and was reading the wrong (`15715de6`) + +### Services +- **Remove per-user active-listings cap from auction house**: Drops MAX_ACTIVE_LISTINGS_PER_USER (= 25) and the cap-check blocks in; create_listing and create_listing_by_token. The browse and mine views; already paginate, so the cap was the only thing it was protecting and (`5da49d79`) +- **AH list/cancel/buy keeps JSONB inventory in sync**: Player asked for items to come out of inventory when listed and go; back when delisted. Token-path AH listings (fish, crops, ore,; weapons, armor, consumables, crafted) escrowed the NFT correctly (`7c213615`) +- **Arena wins credit active-buddy XP**: Same fix as the wild-battle XP credit, applied to the arena.; services.buddy_economy.resolve_arena_battle now looks up the; active buddy on a win and calls services.buddy_battle.award_battle_xp. (`4eb79f6c`) +- **Wild battles credit active-buddy XP (delve + fishing)**: Pre-fix the buddy that actually fought a wild encounter walked away; with nothing on its row -- only the player got RUNE / ore / BBT; (delve) or LURE / REEL / BBT (fishing). The win counter and mood (`b8164c18`) +- **Fix nest deposit IndeterminateDatatypeError**: Real bug found via the now-visible exception in the error reply:; the BUD-burn UPDATE inside buddy_breeding.deposit referenced $1, $2,; $4 -- skipping $3 -- but Python passed 4 positional args. asyncpg (`aa1c198c`) + +### Changes +- **Add CHANGELOG entry for Quick Buy fix** (`22928abc`) +- **Redraw draclet ASCII frames into a proper dragon silhouette** (`ce9e55e7`) + +--- + +## [main] — 2026-05-01 + +### Changes +- **`,bal` Summary: Holdings now spans two columns + adds NFTs, Farming, Crafting**: The Holdings block on the Summary tab was a single tall column missing three categories that already roll into Net Worth. Added 🎨 NFTs (`nft_value`), 🌾 Farming (stake + plot + inventory combined), and 🔨 Crafting (stake + inventory combined) so every component of `compute_net_worth().total` is now visible on the card. The list also splits into two side-by-side columns -- biggest position top-left, smallest bottom-right -- so a player with a deep portfolio doesn't get a 16-line wall of text. +- **`,bal` Summary tab reformed: clean, minimal, informative**: The default Summary card was a wall of up to 22 inline `≈ $X,XXX.XX` fields with the headline (Net Worth + PnL) buried at the bottom. Reformed so the headline now leads as the embed description (`**$XX,XXX.XX** · net worth` with PnL underneath), wallet + bank + optional loan stay on one inline row, and every other position (CeFi / DeFi / Nodes / LP / Rigs / Delegations / Validator / Moon / Savings / Items / Fishing / Delve / Buddy) collapses into a single sorted **Holdings** field listed largest-to-smallest. The two `🌕 Moon` rows (Lunar Mint + Pool) and the two Delve rows (Stake + Party) are fused into one entry each. Progression (achievements / streak / pass / challenges) moves off Summary entirely -- it already lives on the Profile tab. Same data; far less noise. +- **Tighter footer hints across the bot**: Dropped decorative `💡` / `⚡` glyphs from informational footers in `,stake`, `,trade`, `,prices`, `,wallet` (DeFi), and `,admin halt` -- footer text is already secondary subtext, the emoji wasn't carrying meaning. Verbose pipe-separated command lists like `".wallet deposit to fund | .wallet withdraw to move back to CeFi | .wallet list for addresses"` collapse to `".wallet deposit, .wallet withdraw, .wallet list"`. Filter-help wording is now identical across `,trade` and `,prices`. Same information, less noise. +- **"Did you mean...?" suggestions wear a thinking face**: Typo a command name and you'd previously see an amber embed reading "Command `,foo` not found. Did you mean **`,bar`**?" The line now leads with a 🤔 glyph so it's instantly readable as a suggestion (not an error). Same Yes/No view, same Cancel timeout -- the prefix just makes the intent clearer at a glance, especially next to the other red ❌ failure embeds. +- **Slash-command failures now match prefix-command failures**: `on_app_command_error` was sending a raw `discord.Embed` with `❌ ` -- functionally identical to `ctx.reply_error()` but built outside the canonical `card()` pipeline, so it skipped the bot-wide LinkManager / footer normalisation. Slash-command errors now flow through `card()` so the surface looks identical to a prefix-command error. +- **Bot's global error embed (admin error channel) standardized**: The "command crashed" embed that posts into the configured error channel for moderators (`.admin set error_channel`) is now built through `card()` like everything else. The fields are unchanged (Command / User / Error / Traceback / footer), just routed through the canonical builder so future cross-cutting embed changes (color theming, link policies) hit it for free. +- **Self-heal "loop restarted" notifications standardized**: The 🔁 self-heal alert posted into error channels when a stuck task loop is auto-restarted now builds through `card()` instead of `discord.Embed()` directly. No visual change for moderators -- it's the last raw embed in the framework layer being retired so the rule "no `discord.Embed()` outside `core/framework/embed.py`" is now actually true everywhere. +- **Showcase + Hub buttons: cleaner, more predictable layout**: `,me` (Showcase) used to put the Bump button on its own row in the middle of the view (Row 0 picker, Row 1 Bump alone, Row 2 + Row 3 jump groups), which read as a visual gap. The Bump button now sits at the bottom (Row 3) by itself the way one-shot housekeeping actions usually do, and the two jump groups (Items/AH/Lexicon, Craft/Fish/Farm) move up to Row 1 + Row 2 directly under the tab picker. Same buttons, same callbacks -- just no more orphan in the middle. On `,today` (Hub), the Row 2 quick-collect strip (Collect Runs / Hatch Egg / Nest) now uses one consistent green Success style instead of mixing primary blue + green; all three are "claim a reward" actions and reading them as one group is the point. +- **Rarity glyphs centralized in one lookup**: `,craft list` / `,craft info` (colored-circle dots) and `,nfts` (colored-square grid) used to maintain their own private rarity-to-emoji dicts. Both now read from one place: `constants/ui.py::RARITY_DOT` for circle-style displays and `RARITY_SQUARE` for grid-style displays, plus `RARITY_ABBR` for the three-letter code-block column headers. Visual output unchanged for now -- the win is that future rarity-glyph tweaks land in one spot instead of having to chase down per-cog tables. +- **Buddy arena tier colors are now named constants**: `services/buddy_economy.py::ARENA_TIERS` was carrying raw hex literals for Bronze/Silver/Gold/Platinum/Diamond. Promoted to `C_TIER_BRONZE/SILVER/GOLD/PLATINUM/DIAMOND` plus an `ARENA_TIER_COLORS` lookup in `constants/ui.py` so future palette tweaks land in one place and tier embeds in `,buddy arena` / battle results / leaderboards always agree on color. +- **Bot's `bot ...` admin replies build through the canonical `card()` DSL**: Migrated every raw `discord.Embed(...)` in `core/framework/internal_commands.py` (search results, command help, top gainers / losers, market overview, compare tokens, top holders, server stats, explorer summary, networks list / detail, admin audit log) onto `core.framework.embed.card()`. Embeds look identical, but they now share the same builder pipeline as the rest of the bot -- LinkManager processing, footer/timestamp normalisation, and any future cross-cutting changes apply to these surfaces too. +- **Success replies now lead with a checkmark glyph**: Every `ctx.reply_success(...)` (`,buy`/`,sell`/`,move`/`,stake`/`,craft make`/...) now renders a leading `✅` to mirror the `❌` on `ctx.reply_error(...)`. Same green color, same body text -- the symbol just makes win/lose state readable at a glance and matches the visual symmetry the rest of the bot already uses. +- **Same rarity tier, same color everywhere**: Crops in `,farm`, recipes in `,craft`, dungeon loot, and the `,me` Showcase tabs now all resolve their accent color through `constants/ui.py::RARITY_COLORS` instead of each cog hardcoding its own hex. Common is gray, Uncommon green, Rare blue, Epic purple, Legendary gold -- consistently, in every embed they appear in. Fixes the "wait, why does Epic look different here?" mismatch on the showcase Dungeon / Crafting / Farming tabs. +- **Live countdowns everywhere a time is shown**: Inline scheduled / starts-in / ends-in timestamps in `,calendar`, `,today`, `,buddy expedition`, `,delve` (buddy on expedition), and the farm harvest log now route through a new `core.framework.ui.fmt_rel(ts)` helper that emits Discord's auto-updating relative-time markup. Existing displays still render the same "in 3h" / "5 minutes ago" string, but they all live-update in place from one code path now -- no more stale "5 minutes ago" lingering after the page sits open for an hour. +- **Auction House: per-user active-listings cap removed**: The 25-active-listings-per-user limit on `,ah list` is gone. Sellers with deep inventories (fish stacks, multi-recipe crafters, buddy breeders) no longer have to cancel an existing listing before posting a new one. The browse / mine views already paginate, so the cap was the only thing it was protecting and it isn't worth the seller-side friction. + +### New Features +- **Quick Buy modal on every per-game shop**: `,farm shop`, `,fish shop`, `,delve shop`, and `,buddy shop` now mount the same Quick Buy button that lives under `,shop` -- click it to pop a modal, type what you want, hit submit. The "Pays in" field is pre-filled with the single currency that shop accepts (`HRV` for farm, `REEL` for fish, `RUNE` for delve, `BUD` for buddy) and editing it to anything else is rejected before dispatch, so no more accidental "wait, what does this spend?" buys. The modal re-dispatches a synthetic `, buy ` through the normal command pipeline so every cooldown / `ensure_registered` / `ConfirmView` step runs exactly as if the player had typed it. Shared logic lives in `core/framework/quick_buy.py` (`QuickBuyModal` / `QuickBuyButton` / `QuickBuyView`) so adding the same button to a future shop is a one-line wire-up. +- **`,admin buddy recover `**: New admin lookup that answers the "I caught a buddy in this message but I can't find it" reports directly. Wild captures now stamp the battle-result message id + channel id onto `cc_buddies.capture_message_id` / `capture_channel_id` (migration 0193). The command accepts a raw snowflake or a full Discord message link, prints owner / species / rarity / level / status / capture channel / hatched_at, and tells the admin which `,buddy find ` invocation will surface the buddy in the player's collection. Legacy rows pre-dating the migration carry NULL message ids and fall back to the existing `,buddy find` workflow. +- **Crafting cards link to the Auction House**: `,craft make` and `,craft info` now include a "Sell on Auction House" field with the exact `,ah list ` syntax for the recipe and the last-sold price (currency + USD) pulled from `item_token_events`. Players who craft a profitable surplus see the listing command and the going rate without leaving the receipt; recipes that have never traded show "No prior sales -- you set the floor". Backed by a new `services.auction.last_sold_price(contract_address)` helper so any future panel can show last-traded prices off the same query. +- **`,buddy find `**: New search command that scans every status (owned active, owned inactive, storage, shelter) and groups matches by where the buddy lives. Resolves the recurring "I caught a wild buddy but I can't find it" report -- wild captures land in your owned roster as inactive, so they don't appear at the top of `,buddy stats` and players assumed they had vanished. With no query the command lists every buddy you have. Each row shows id, emoji, name, gender glyph, level, rarity tier, and species; each section ends with the exact follow-up command (e.g. `,buddy retrieve ` for storage rows, `,buddy adopt ` for shelter rows). + +### Database +- **Migration 0193 (`buddy_capture_origin.sql`)**: Adds nullable `capture_message_id BIGINT` + `capture_channel_id BIGINT` to `cc_buddies`, plus a partial index on `capture_message_id WHERE NOT NULL` so `,admin buddy recover` is O(log n). Wild captures from fishing populate both columns going forward; legacy rows and non-fishing capture paths (daycare hatch, breeding, admin spawn) carry NULL. + +### Bug Fixes +- **`,today` panel: rename "Daycare" -> "Nest" and stop double-listing unspent stat points**: Two long-standing copy bugs on the unified Home / Today panel. (1) The Buddy tab still labelled the breeding surface "Daycare" everywhere -- jump button, action button, summary field title, "Egg ready / incubating / empty" lines, the Guide cheatsheet, the Next-Steps nudge, and the `,help` blurb -- even though the canonical command renamed to `,buddy nest` months ago (`daycare` is just a back-compat alias). Every player-visible string and pointer command now reads `Nest` / `,buddy nest ...`, matching the actual cog surface. (2) Unspent delve + buddy stat points were rendered twice: once in the dedicated **Stat points** field and once again at the top of **Ready right now** because `services/hub.py` was prepending the same lines into `ready_hints`. The prepend is gone -- the dedicated field is the single source of truth. +- **Fix `,shop` Delves dropdown silently doing nothing**: Selecting "Delves" in the `,shop` dropdown rebuilt a single embed packing all 44 weapons, 25 armor, and 29 consumables into three fields. Each field blew past Discord's 1024-char-per-field cap (and the embed itself trampled the 6000-char total), so `message.edit` 400'd and the on_select handler swallowed the HTTPException -- the embed never changed. The Delve shop now renders as one intro embed plus one embed per category (Weapons / Armor / Consumables), with each catalog packed into chunked fields that stay under 1024 chars apiece. +- **Buddy storage list now shows rarity tier (gender already present)**: `,buddy storage` rows previously rendered name + gender + level + W-L only; the rarity tier was fetched but never surfaced. Each row now reads `#id emoji name♂ - Lv.N Tier - W-L`. `services/buddy_lifecycle.list_storage` also now selects `gender` so the data layer matches the display. +- **Wild-capture success message no longer says "joined your shelter"**: The fishing wild-capture line claimed the captured buddy "joined your shelter" -- but captures actually go into the owner's main roster as inactive (status `owned`, `is_active = FALSE`), not into the shelter table. Players who tried `,buddy shelter` to find their capture saw an empty list and reported the buddy as missing. The line now says "joined your collection", includes the new buddy id and gender glyph, and points at `,buddy stats` (page through) plus `,buddy find ` for follow-up. +- **Surface "capture refused -- you're at the buddy cap"**: Wild battle wins silently swallowed the capture roll when the player was already at their owned-buddy cap, so the receipt only showed the LURE/REEL payout with no signal that a capture had been rolled and refused. `WildBattleResolution` now carries `capture_refused_full` + `owned_cap`; the cog renders an explicit "Almost!" line that names the cap and lists the three ways to free a slot (`,buddy store`, `,buddy surrender`, `,buddy slot buy`). + +### Services +- **Fix harvest_pie xp_big effect + diamond_pickaxe mine boost**: Bug #330 (player report, Chef Jihadi): ,craft apply harvest_pie raised; "Unknown buddy effect: xp_big" because services/crafting.py:_apply_to_buddy; only handled 'xp' (+500 XP). harvest_pie advertises +1500 XP via (`c64b2cac`) +- **Surface real exception on backfill failures + fix FOR UPDATE**: services/items.py:; - _next_unit_index used db.execute() for SELECT FOR UPDATE. asyncpg; treats execute() on a SELECT as a no-op for row-locking semantics (`30ed2084`) +- **Nft phase 2 pr4: ah listing reuses existing token, no duplicate mint**: services/auction.py:create_listing now calls a new; _resolve_token_for_listing helper that:; - Maps (kind, ref/metadata) to a contract address. (`6b45e2df`) +- **Nft phase 2 pr2+3: wire farming + dungeon**: services/farming.py:; - harvest_plot mints crop. tokens (one per harvested unit).; - sell_crop burns crop. tokens per sold unit. (`de82f32b`) +- **Nft phase 2 pr1: wire fishing through services/items**: Adds best-effort NFT-layer sync next to every JSONB write in; services/fishing.py. JSONB is still source of truth this PR; the; per-unit tokens shadow it so the contract counters track real (`23ddc7c7`) + +### Bug Fixes +- **Fix `,shop` Quick Buy modal failing with "interaction failed"**: Two latent bugs in the Quick Buy flow: the modal was shadowing discord.py's internal `view` attribute (broke the submit lifecycle) and on submit it was direct-invoking the `,shop buy` Command object with the original `,shop` context (skipped every decorator and crashed on missing `ctx.user_row`). Quick Buy now opens cleanly and re-dispatches a synthetic `,shop buy [currency]` message through the full command pipeline so the normal ConfirmView purchase flow runs. + +### Changes +- **`,today` / `,start` and `,shop` panels restyled with stat-row bars**: The unified Home dashboard and every Item Shop tab now lead with a status block of progress-bar stat rows (wallet vs net worth, daily/streak progress, per-game milestones, wallet vs cheapest-stone) so players can read their position at a glance instead of scanning bullet lists. Shop pages also pin the wallet block to the top, attach a thumbnail, and the per-stone owned line uses the same level-progress bar. Backed by a new `FormatKit.stat_row()` helper so any future panel can match the look in one call. + +### New Features +- **`,admin pump` becomes a full chart-pattern engine**: The pump command now drives prices through 25 named curves -- pump, moon, dump, crash, bull, bear, volatile, wave, rugpull, pumpdump, vshape, hns, double_top, double_bottom, cup_handle, bullflag, bearflag, chaos, zigzag, spike, accumulate, distribute, stairstep, fakeout, and the original linear -- over any 1 min to 24 h timeframe, with `random` as a dice-roll. Targets expand from one symbol or `everything` to category selectors (`coins`, `stables`, `group`, `earn`, `wrapped`, `pow`, `pos`, `builtin`, `meme`), `chain:` for everything on a network, and a chaos mode (`each` / `each:`) that gives every token its own random pattern, magnitude, and duration so the entire chart wall lights up at once. New helpers `,admin pump patterns` and `,admin pump active` document the catalog and surface running events. +- **`,farm fertilize all` + Fertilize All button**: One call now applies the equipped fertilizer to every growing plot that doesn't already have one, stopping when the inventory runs out. Reachable both as a slash arg (`,farm fertilize all`) and as a new row-1 button on the FarmFieldView so you don't have to fertilize plots one slot at a time after a Plant All. +- **One-time welcome DM on first interaction**: New players receive a single introductory DM the first time they trigger any registered command, covering what the bot does, the messages it may send, every off-switch (`,notify`, `,optout`, admin module toggles), the privacy/disclaimer, and a contact link to the maintainer (<@801280612111482890>). Dedup is per Discord user across guilds via a new `welcomed_users` table; if DMs are blocked the row is left unstamped so the message can deliver later. The DM names the server the player came from. +- **First-time game intros from Disco**: The first time a player opens `,farm`, `,fish`, `,delve`, `,buddy`, `,trade`, `,ah`, `,craft`, `,expedition`, or `,play`, Disco posts a single intro card explaining the surface and pointing to the right next commands. Tracked in the new `user_module_seen` table; later visits are no-ops. Wired through `services/onboarding.maybe_send_intro` so adding a new game later is a one-line call. +- **Beefed-up `,delve stats` panel**: Added INT alongside ATK / DEF / SPD, a dedicated Allocations field showing per-stat HP / ATK / SPD / INT spend + unspent stat points, a Skill field with name / multiplier / cooldown state / kind tags, equipped Weapon line that surfaces type + attack kind + ATK bonus + ammo count for ranged builds, equipped Armor with type + DEF bonus, and an Active Buffs field that lists Thorn Aura / Wildshape / Sanctuary / Marked Target / Volley Charged / Regen with remaining round count. Stats counters stayed where they were. + +### Bug Fixes +- **Fix `,admin pump active` 6000-char embed overflow**: With many concurrent events (e.g. after `,admin pump each:builtin` schedules ~25 tokens at once) the active-events view exceeded Discord's 6000-character / 25-field embed limits and the bot returned `400 Bad Request: Embed size exceeds maximum size of 6000`. The roster is now compact one-line-per-event, paginated 15 per page via `send_paginated` so the embed never trips the limit no matter how many events are running. +- **Fix ,delve stats opening the room embed**: ,delve stats was routing the caller to the active-room embed whenever they were mid-run, making the actual stats panel inaccessible during a delve. Bare ,delve already shows the room when applicable, so ,delve stats now always renders the stats panel. + +### Database +- **Migration 0192 (`player_onboarding.sql`)**: Adds `welcomed_users(user_id, sent_at)` for global welcome-DM dedup and `user_module_seen(user_id, module, first_seen_at)` for first-time game intros. Both keyed on user_id only -- the welcome message is "first interaction with the bot", not "first interaction in this guild". + +### Discord Bot +- **Delve consumables / farm Plant All / cast-result bait dropdown**: cogs/dungeon.py:; - _DelveConsumableSelect (row 2 on _DelveRoomView): lists every; consumable the player owns with qty; on select runs (`27d7276a`) +- **,shop hub Quick Buy + Refresh; ,buddy panel List on AH**: cogs/shop.py:; - _ShopHubView gets two row-1 buttons:; * Quick Buy (success) -- pops _ShopBuyModal asking for item key (`305052cf`) +- **,me showcase becomes the panel navigation hub**: cogs/showcase.py:; - New _PanelJumpButton: owner-locked, on click sends an ephemeral; hint with the panel's exact command + one-line blurb. (`5d055e8a`) +- **Prices in ,db browse + USD column in ,ah browse / search**: cogs/lexicon.py:; - ,db browse SQL now joins each contract's most-recent sold event; (last_sold CTE) so each row can render price inline. (`fc69b15e`) +- **Farm plot-target select + fish cast-result Panel button**: cogs/farming.py:; - New _PlotTargetSelect (row 3) on FarmFieldView. Lists every; plot with state + crop emoji + name; "Next empty plot" default (`b795bb6d`) +- **Dropdowns on ,fish stats + ,farm; ,inventory footer hint**: cogs/fishing.py:; - New _FishStatsView (owner-locked, 5-min timeout) wraps the; ,fish stats panel for self-views. (`bc39c596`) +- **,craft list is an interactive recipe browser**: Inspired by the _DelveRoomView mines-panel pattern. Owner-locked,; 5-min timeout. Components:; cogs/crafting.py: (`e249ad21`) +- **Ah browse picker dropdown + items inspect Transfer modal**: cogs/auction.py:; - New _BrowsePickSelect (row 2) populated dynamically from the; current page slice. Selecting a row opens the full (`f5b2ae25`) +- **Action buttons on ,ah inspect + ,items inspect**: cogs/auction.py:; - New ListingActionView for ,ah inspect with three buttons:; Buy, Cancel, Refresh. Owner-locked, 5-min timeout. Buy is hidden (`abde2735`) +- **Ah list: three-form parser (id / token_id / name); drop legacy**: The ,ah list parser had a buddy-id case that fell through to the; legacy form, which then errored with "Need: ; [qty] " -- confusing nonsense for a player who typed (`dac88018`) +- **Fix empty-state hint to use ,ah list form**: cogs/auction.py: the ,ah browse / search empty-state was still; showing the legacy form. Swapped to the new; form with the minnow example so the muscle memory (`631f27f9`) +- **Skip JSONB on token-path cancel/buy; lexicon footer sell hint**: services/auction.py:; - create_listing_by_token now stamps metadata.path = "token" on the; listing row so the cancel + buy paths can tell which listings (`da00f1a8`) +- **,group lp -- founder treasury <-> LP with safeguards**: Migration 0183_group_treasury_lp.sql:; - mining_groups gains treasury_lp_unlocked (master kill switch),; last_treasury_lp_at (cooldown anchor), treasury_lp_total_raw (`c86a8883`) +- **Interactive browser with kind dropdown + pagination + sort**: cogs/nft.py:; - New ItemsView (discord.ui.View) replaces the static ,items; overview embed. Owner-locked, 5-min timeout. Mirrors the (`3061cdd3`) +- **,ah list + NFT-only listing path**: services/auction.py:; - create_listing_by_token rewritten to be a standalone path that; doesn't go through _lock_* JSONB handlers. Escrows the NFT (`5926e248`) +- **Gas fees on every NFT state change**: Every player-initiated transition (transfer / list / unlist / sold); now pays gas in the network's native coin -- the way real NFT chains; work. Mints + burns stay free since those are gameplay outcomes, not (`119337b0`) +- **Per-token history + per-item/per-kind price displays**: Migration 0180_item_token_events.sql:; - New item_token_events table -- one row per token state transition; (mint / transfer / list / unlist / sold / burn) with from/to user (`f1af6a81`) +- **Token-id-driven listing (,ah list fge:fd67e9ee 100)**: services/auction.py:; - New create_listing_by_token(token_id, price, currency=...) that; reads kind / catalog_key straight off the contract registry. (`c2916af3`) +- **,db item lexicon + premium third specialty slot**: cogs/lexicon.py + services/lexicon.py (the item lexicon):; - ,db / ,lexicon / ,wiki / ,find / ,lookup -- player-facing; catalog browser backed by item_contracts. (`6946fad6`) +- **Admin items reconcile: chunk drift field to stay under 1024 chars**: Discord caps embed field values at 1024 characters; 30 drift rows; at ~100 chars each blew past it and the embed POST returned 50035; "Invalid Form Body" instead of the report. (`df36ceda`) +- **Render full network name in inspect + contract panels**: cogs/nft.py adds a _NETWORK_FULL short->full map (bud -> "Buddy; Network", lur -> "Lure Network", har -> "Harvest Network", etc.); and a _network_full() helper. ,items inspect and ,items contract (`27a6ae57`) +- **Nft hotfix 3: drop backfill self-sabotage + ,admin items backfill**: The reconcile screenshot showed drift +N for every kind that had; existing inventory: jsonb=N nft=0. Root cause: on the first buggy; boot (when mint_unit was broken from the contracts.rarity_tier (`5750af75`) +- **Nft hotfix 2: ,nft -> ,items + shop overflow**: cogs/nft.py:; - ,nft is already the legacy NFT-collection command (cogs/nfts.py).; Renamed my new player-facing command to ,items (aliases ,bag / (`582be544`) +- **Nft hotfix: admin command collision + items.py table-rename leak**: services/items.py:; - upsert_contract's ON CONFLICT DO UPDATE SET clause referenced; contracts. for three columns (rarity_tier, base_price_raw, (`edbb8b3a`) +- **Nft phase 2 pr7: ore-fungible, multi-qty ah escrow, ,admin nft reconcile**: Design call (your three-question answer):; - Ore stays fungible. COPPER / SILVER / GOLD live in wallet_holdings; with oracle prices + stake yields + AMM trades. Per-unit NFTs (`f76e4316`) +- **Nft phase 2 pr6: ,nft commands -- list / inspect / contracts / transfer**: cogs/nft.py exposes the per-unit NFT layer to players:; - ,nft -- overview by kind; - ,nft list [kind] -- owned tokens grouped by contract (`8b35f290`) +- **Nft phase 2 pr5: consume + transfer for guards, stones, buddies**: database/users.py:; - use_validator_guard / use_yield_guard burn the matching shop.; NFT after the inventory decrement. (`f277dd12`) +- **Nft phase 2 pr3: wire crafting + shop**: services/crafting.py:; - craft_item mints crafted. tokens per produced unit.; - _decrement_crafted burns crafted. tokens on apply. (`8cfc84d8`) +- **Surface real settle errors + buddy-by-name fix; eggs genderless**: - cogs/auction.py: ,ah buy and ,ah list catch-all now print the actual; exception class + message instead of the generic "try again", so a; player hitting a constraint or asyncpg error has something to report. (`d42e48f5`) +- **Fix ,ah crash: metadata string not parsed as dict**: Six sites in cogs/auction.py used (row.get('metadata') or {}) which; returns the raw JSON string when metadata is non-empty (truthy string; bypasses the or {} fallback). All six now call auc._as_dict() which (`17d8bb30`) +- **Add farming field buttons + fix fishing hook timer**: - FarmFieldView: Harvest All / Water All / Sell All / Refresh + Bump; buttons on the ,farm embed; each action runs in-place and refreshes; the field view without requiring a separate command. (`030b7c11`) + +### Database +- **Mig 0182: widen item_instances_kind_chk to allow bait/junk/shop/stone**: Migration 0176 added 'bait', 'junk', 'shop', 'stone' to the; item_contracts kind-allowlist but didn't mirror the change on; item_instances. The original CHECK from migration 0173 still only (`3a108974`) +- **Nft phase 1: per-unit token layer + contract registry + backfill**: Promotes item_instances from a lazy auction-house artefact to a full; per-unit NFT layer with a real contract registry. Every item the bot; can mint is now a deployable contract; every individual unit (one (`2f0aaa75`) + +--- + +## [main] -- 2026-05-01 + +### Bug fix: delve shop weapons category exceeded Discord embed cap +- **`,delve shop` Weapons (all) crashed with `Embed size exceeds maximum size of 6000`**. The expanded weapon catalog (28 entries spanning longsword / shortsword / axe / mace / bow / crossbow / staff / rod) plus the multi-line stat formatter pushed the rendered embed past Discord's 6000-character total embed limit. Added pagination -- 8 items per page with ◀ / ▶ buttons on row 1, page count in the title (`Surface Shop -- Weapons (all) (page 1/4)`), and an X-of-Y footer line. Page resets to 0 on category swap; cursor snaps to last page when the category change shrinks total. Same UX, no more crashes. + + + +### Expedition lock now covers wild buddy battles + crafted treats +- **Wild buddy Challenge buttons (delve + fishing) refuse when the active buddy is busy or every buddy is away.** Engage path queries the buddy roster: if all owned buddies are currently on expeditions, the wild encounter ends with a tailored "All N of your buddies are out on expeditions -- there's no one home to fight" message; if some buddies are home but the active one is deployed, it nudges the player to swap active via `,buddy`. No partial state -- the wild buddy escapes either way. +- **Crafted-food / treat applies (`,craft apply `) refuse on a deployed active buddy.** `services.crafting._apply_buddy_effect` now runs the same `buddy_expeditions` busy probe before any feed / play / restore / xp / xp_big / feast / full_revive / reroll_rarity effect, so a player can't bypass the panel-side feed/pet/talk lock by routing through the crafting cog. Same error copy as the panel: "Your active buddy is on an expedition -- can't feed treats or apply consumables until they're back." + + + +### Fishing: REEL button now sits beside HOOK +- The secondary REEL action button was on row 1, forcing the eye to jump rows mid-cast when the prompt fired. Moved to `row=0` so HOOK and REEL share the same row -- one glance, one decision, no jumping. + + + +### Draclet (mini-dragon) ASCII art redrawn +- The `draclet` species used a generic blob silhouette (`/\_/\` head, `\_/\_/` paws) that read as Winnie the Pooh more than a dragon. Redrawn all seven mood frames with a proper draconic outline: spiked wings flared off the shoulders, a long snout, an underbelly, and a coiled tail tipped with a barb (`)___)>~~`). Same 7-line height so the panel layout doesn't shift. + + + +### Bug fix: today panel quests rendered as `?` +- **`,today` Top quests showed `?` for every name**. Quest rows from `services.quests.current_for_user` carry only the per-user counters (`quest_id`, `progress`, `target`, `claimed`) -- the human-facing `name` + `icon` live on the static template in `quests_config.QUESTS` and have to be looked up by `quest_id`. The unified panel was reading `q.get("name")` directly, which is always None, so the fallback `?` rendered. Now the panel hydrates the template via a `{quest_id: tmpl}` map and pulls the icon / name / period from there, falling back to the row dict only if the template is missing. Also restores the `· Daily` / `· Weekly` period tag the legacy `,today` panel showed. + + + +### Bug fix: expedition lock query referenced wrong column +- **`,buddy upgrade` (and any feed/pet/talk/battle/arena path) was crashing with `column e.id does not exist`**. The expedition-busy LEFT JOIN added in the previous fix referenced `e.id` for the buddy_expeditions table; the actual primary key column on that table is `expedition_id`. Updated `_fetch_active` in `cogs/buddy.py` and the cross-cog buddy bonus query in `services/buddy_bonus.py` to use `e.expedition_id IS NULL` / `e.expedition_id IS NOT NULL` so the on-expedition flag computes correctly. Now expedition locking works as intended without breaking every buddy interaction. + + + +### Server calendar with mines-style button grid +- **New `,calendar` command (alias `,agenda`, `,schedule`)** opens an interactive embed grid that lists every active server-wide challenge, the currently-running market event (Pump / Crash / Moon / Rugpull / etc), and the next daily / weekly recurring resets. Tile layout mirrors the `,mines` game: up to 20 calendar entries laid out 5-per-row across 4 rows, with a Refresh + Close pair on row 4. Tile color encodes the kind -- **blurple primary** for challenges, **green success** for live market events, **gray secondary** for recurring resets -- so live items pop visually. +- **Tap a tile for details**. Each tile pops an ephemeral embed showing the full description, start time, end time (with native `` + `` Discord timestamps), and -- for challenges -- the live progress + reward pool. Refresh re-pulls fresh data; Close strips the view leaving the embed in place. +- **`,today` panel surfaces the top 5 calendar items inline + a Calendar button**. The unified today / start dashboard now renders a `Calendar` field listing the next 5 live + upcoming items with `` countdowns, and a row-3 `🗓 Calendar` button that opens the full grid in place. `HubSummary` carries `calendar_items: list[CalendarItem]` populated from `services.calendar.list_calendar` so both the embed and the home view fetch from the same source. +- **Calendar auto-posts when admins schedule something**. `,admin event trigger ` and `,admin challenge start ...` both call `cogs.calendar.post_calendar_to_bot_channel(bot, guild)` after the original embed lands, so a fresh calendar drops in the guild's configured events / bot / system channel. Best-effort -- failures don't abort the admin command. +- **`services.calendar.list_calendar`** is the single source of truth: aggregates `services.challenges.list_active`, the active market event from Redis (`cogs.events.get_active_event`), and computes the next daily 00:00 UTC + Monday 00:00 UTC reset purely off the wall clock. Sorted live-first, then by next-scheduled time. The same helper backs `,calendar`, the `,today` Calendar field, and (future) the API surface. + + + +### Game embeds no longer auto-deleted, delve shop is now a dropdown +- **Game UI embeds skip the `reply_delete_after` auto-delete**. Was a bug where any guild that set a non-zero `reply_delete_after` would have its delve room views, fish cast views, buddy panels, AH browsers, and the unified `,today` panel torpedoed mid-play -- buttons stop working as soon as the message vanishes. `core.framework.context.DiscoContext.reply()` and `.send()` now classify any reply that ships with a `view=...` as persistent and skip the autodelete schedule. Plain text / cooldown / error replies still respect the guild setting (so a hundred cooldown notices don't pile up). New `no_autodelete=True` kwarg lets callers opt out explicitly when a viewless embed (e.g. trade receipt) should also outlive the autodelete window. +- **Delve shop is a dropdown-driven browser**. Was one ~2,000-character embed that dumped every weapon, armor, and consumable side by side -- ATK / DEF / heal % stats jumbled with raw config values, no class filter. Replaced with an interactive `_DelveShopView` carrying a category dropdown (Weapons -- your class / Weapons -- all / Armor -- your class / Armor -- all / Healing / Buffs / Damage Scrolls / Ammo / Utility). Default page is class-filtered weapons so a Mage doesn't see plate. +- **Item stats now render cleanly per kind**. Each line shows emoji, name, type tag (longsword / staff / light / heavy etc.), the relevant stat in human terms (`+12 ATK`, `+18 DEF`, `+25% HP`, `5x ATK damage`, `20 shots (+25% dmg)`, `+10% HP/turn · 5r`), the price in RUNE, and the catalog blurb on a `-#` subline. No more guessing what `value: 0.25` means. +- Class label rendered in the description so the player can see which weapon / armor types they can equip without bouncing to `,delve class`. Buttons / commands unchanged -- buy with `,delve buy ` as before. + + + +### Unified `,today` / `,start` interactive panel +- **`,today` and `,start` now open the SAME panel**. Both commands route through `cogs.overview.open_unified_panel` which fetches both the onboarding state (wallet / net worth / starter / progress) and the live `HubSummary` (streak / quests / ready hints / stat points / activity rollup) in one shot, then mounts the existing tabbed `StartInterfaceView` on the result. The Home tab is a single embed with every today field folded in. The eight other tabs (Guide / Wallet / Market / Fishing / Farming / Delve / Buddy / Crafting / Stones) all keep their existing in-place action buttons so players can fish, plant, delve, hatch, or trade directly from the same message without typing any prefix command. +- **Home tab buttons are now state-aware**. New ready-feed quick-collects appear when applicable: `Collect Runs` (when expeditions are ready), `Collect Eggs` (when nest is ready), `Harvest` (when plots are ripe), `Traps` (when crab traps are ready to haul), `Spend Pts` (when delve stat points are unspent), `Buddy Pts` (when buddy stat points are unspent). Onboarding nudges (Hatch / Fish / Plant) drop off automatically as the player completes them. Daily and Starter Pack still occupy their priority slots. The home embed footer reads `,today to reopen · switch tabs to play any game from this panel · buttons run the listed command as you'd type it`. +- **Legacy standalone `,today` panel removed**. The `,today` command in `cogs/hub.py` now delegates to `open_unified_panel`. The old `_HubView` + `_build_embed` are kept for any subsystem that imported them, but no command opens them anymore. +- Refresh button on every tab (including Home) re-fetches both streams so an action taken via a tab button updates the panel in place; `,today` no longer needs to be rerun after claiming daily / collecting an expedition. + +### Wild battle XP, expedition lock, expedition payout bump +- **Buddy XP from wild / arena battles is now visible in the result embed**. The plumbing existed (services credited XP via `award_battle_xp` and stored it on `WildBattleResolution.buddy_xp_awarded` / `ArenaResolution.buddy_xp_awarded`) but no surface ever showed it. Delve `_render_final_embed`, fishing `_render_final_embed`, and the arena win path now print `🐶 Your buddy (#id) earns +N XP.` so players see the same XP hit they get from a buddy-vs-buddy battle. +- **Expeditions still running are out of "Ready right now"**. Was rendering "1 expedition still running -- check time left" in the ready feed even though it isn't claimable yet. Moved to the **Activity** rollup ("1 expedition in progress -- `,expedition` for time left") which is where AH listings + active quests + active challenges already sit. Ready feed is now only for actually-actionable items. +- **Buddies on expedition are locked from in-game interactions**. While a buddy is on a running expedition the cog refuses feed / pet / talk / PvP battle / arena battle with `**** is away on **** (back ). Buddies on expeditions can't be fed, petted, talked to, or sent into combat until they return.` `_fetch_active` now LEFT-JOINs `buddy_expeditions` and surfaces `on_expedition`, `expedition_ends_at`, `expedition_destination` so every call site can guard with the new `_expedition_busy_message()` helper. The cross-cog buddy bonus path (`services/buddy_bonus.buddy_bonus`) also skips a buddy on an expedition so passive bonuses for chat / work / fish / farm / delve aren't farmed off a deployed buddy. The existing partial-unique index on `buddy_expeditions(buddy_id) WHERE status = 'running'` already enforced "one expedition at a time" per buddy. +- **Expedition loot bumped ~30-40%**. Per-draw ore ranges and RUNE ranges for every destination raised so a 12h Mine run on a mine-affinity buddy nets ~18-30 ore + 6-15 RUNE (up from 12-18 / 4-9). The "nothing" dead-loot bucket also halved (from 10-15% → 5-8% per draw) and the productive buckets pick up the slack so every expedition feels like it pulls something. Same destination ratios, just bigger numbers + fewer empty pulls. + + + +### Delve overhaul phase 4: today panel surfaces unspent stat points +- **`,today` now shows unspent player + buddy stats**. New `Stat points` field on the today embed lists the player's unspent delve stats with their class name (`📊 N unspent as a archer -- ,delve upgrade`) and the SUM of unspent buddy stats across the whole roster (`🐾 N unspent buddy stat points -- ,buddy upgrade`). Always renders -- even at 0 -- so players who haven't engaged with stat allocations see the system exists. When 0 it shows the all-spent confirmation instead so players know they're up to date. +- **Top-of-feed prepend on the ready hints**. If the player has unspent points, the `Ready right now` section gets a top-of-list nudge ahead of the existing nest / expedition / plot lines, so a freshly-leveled-up player can't miss the spend prompt the next time they open `,today`. +- **`HubSummary` gains `delve_unspent_stats / buddies_unspent_stats / delve_class_key` fields**. Two new probes (`_delve_unspent_stats`, `_buddies_unspent_stats`) compute the counters server-side via a single COALESCE-SUM query each so the panel doesn't hydrate the whole roster. Both wrapped in try/except so a missing migration just zeroes the field instead of 500-ing the entire hub. + +### Delve overhaul phase 3: class craftables, quests, achievements +- **Crafting catalog gains 11 class-themed recipes**. `arrow_bundle_craft` + `broadhead_bundle_craft` (fletching) cover bow ammo; `bolt_bundle_craft` + `piercing_bolts_craft` (smithing) cover crossbow ammo; `scroll_volley_inscribe` + `scroll_mark_target_inscribe` (enchanting) cover archer scrolls; `thorn_aura_brew_craft` + `wildshape_potion_craft` + `regrowth_brew_craft` (alchemy) cover druid brews; `mana_draught_distill` + `scroll_sanctuary_inscribe` cover mage utility. Each routes to its `consum/*` apply target so the consumable stack tops up the right inventory column without crafting needing class awareness. +- **Class-aware bus events**. `services.dungeon` + `cogs.dungeon` now fan out per-class trigger names alongside the existing generic ones: `delve_kill_`, `delve_capture_`, `delve_skill_` (volley, wildshape, fireball, cleave, backstab), `delve_class_picked` + `delve_class_picked_`, `delve_class_lv10_`, `delve_stat_spent`, `craft_ammo`. Quests + achievements key off these directly so per-class progression counts cleanly without a new payload-aware filter mechanic. +- **6 new daily / weekly quests**. Daily: Loose A Volley (3x Volley), Wild At Heart (3x Wildshape), Quiver Stocked (5x ammo crafts), Sharpen Up (any stat point spent), Reinvent Yourself (any class reroll). Weekly: Marksman (25 kills as Archer), Speak To The Wild (3 captures as Druid). +- **9 new achievements**. Apprentice Archer + Apprentice Druid (first class pick); Volley Master (25 volleys) + Beast Speaker (25 wildshapes); Min-Maxer (50 total stat points spent); Class Chameleon + Identity Crisis (1 + 5 rerolls); Iron Veteran / Channeler / Shadowstep / Sharpshooter / Forest Walker (Lv. 10 in each respective class). + +### Delve overhaul phase 2: ranged combat, class skills, stat-point spending +- **Ranged vs melee combat split**. `services.dungeon.resolve_attack` was a single SPD-tied turn-order loop; now it branches on the equipped weapon's `attack_kind`. Ranged weapons (bow / crossbow) take their first swing **before** the mob regardless of SPD (kiting), get +5% crit on the opener, and the mob's counter-swing in the same round only deals 85% damage (`RANGED_RETALIATION_MULT`). Melee combat keeps the legacy SPD-tie behavior. +- **Ammo system**. Bow weapons consume one `arrow_bundle` per swing (3 for Volley). Crossbows pull from `bolt_bundle` the same way. Out-of-ammo ranged shots auto-fall back to 50% damage (`OUT_OF_AMMO_DAMAGE_MULT`) so running dry is a soft tax, not a hard lockout. New ammo variants (`broadhead_bundle`, `piercing_bolts`) bump per-shot damage by 25-30% via `ammo_dmg_mult`. Direct `,delve use arrow_bundle` is rejected -- ammo only feeds the weapon during a swing. +- **Volley (Archer)** and **Wildshape (Druid)** class skills. Volley = 3 arrow swings at 0.7x each + bonus crit (~2.1x effective with crit upside, burns 3 ammo). Wildshape = single 2.0x beast-strike that heals 15% max HP. Both routed through `resolve_attack`'s `mode='skill'` branch with their own ammo/regen logic. +- **New buff / regen / skill_reset consumable kinds**. `scroll_volley` (charges next basic ranged attack to 3 shots), `scroll_mark_target` (next 3 attacks auto-crit), `thorn_aura_brew` (reflect 30% melee dmg for 4 rounds), `wildshape_potion` (+50% ATK + heal 5% HP/turn for 3 rounds), `regrowth_brew` (+10% HP/turn for 5 rounds), `mana_draught` (resets class-skill cooldown), `scroll_sanctuary` (halve incoming damage for 2 rounds). Buffs persist across rooms via the new `player_buffs` JSONB column on `user_dungeon` (migration `0191`), tick down per combat round, and stack only by overwriting with the longer remaining duration. +- **Stat-point spending lives in `,delve upgrade`**. Mirrors `,buddy upgrade`: 4 lanes (Hardiness +4 max HP, Power +0.6 ATK, Vigor +0.005 SPD, Wisdom +0.6 INT) with sticky allocation across reroll / equip / run lifecycle. INT is folded into spell-skill damage (`Fireball`, `Wildshape`) so Mage / Druid have a meaningful spend lane. Hardiness ALSO bumps current HP by the same delta on spend so allocating mid-rest doesn't leave you missing health. +- **`,delve reroll ` ships**. Geometric cost (`5,000 USD * 2^N`), 6-hour cooldown gated DB-side via `EXTRACT(EPOCH FROM (NOW() - last_class_reroll_at))`. Refused mid-run; preserves level / XP / captures / inventory / stat-point allocations; snaps the new class's starter weapon + armor into the inventory and equips them. A Vigor-stacked Rogue can carry that build cleanly to Archer. +- **Spell skills now read INT**. Mage Fireball + Druid Wildshape damage adds `INT * 1.2` on top of base ATK so the Wisdom investment shows up in actual swing damage rather than being decorative. Physical skills (Cleave / Backstab / Volley) keep their pure-ATK math. + +### Delve overhaul phase 1: archer, druid, weapon/armor types, stat points +- **Two new delve classes**. `,delve class archer` and `,delve class druid` now ship alongside warrior / mage / rogue, each with their own combat profile, weapon restriction, and starter kit. Archers run Bow / Crossbow + medium leather; Druids channel Rod + light robes; Warriors stay sword/axe/mace + heavy plate; Mages stay Staff + light robes; Rogues stay Shortsword + medium leather. Migration `0190` widens the `user_dungeon.class_chk` constraint so the new classes can be persisted. +- **Weapon + armor type taxonomy**. Every entry in `WEAPONS` is tagged with `weapon_type` (`longsword/shortsword/axe/mace/bow/crossbow/staff/rod`) and `attack_kind` (`melee/ranged`). Every entry in `ARMOR` is tagged with `armor_type` (`light/medium/heavy`). `services.dungeon.equip_item` now refuses class-incompatible gear with a human-readable hint (`"A Mage can't wear Plate Armor (heavy armor). Allowed: light."`). No more accidentally equipping a mythril plate as an Archer. +- **New gear catalog**. 8 bows, 5 crossbows, 8 staves, 7 rods, plus 7 light robes and 8 medium hides spanning tiers 0-11 (apprentice through endgame). The fee curve mirrors the existing sword line so cross-class economy stays balanced -- e.g. an Archer's tier-5 elven longbow runs the same RUNE as a Warrior's mythril sword. +- **Stat-point allocation now lives on `user_dungeon`**. New columns `hp_alloc / atk_alloc / spd_alloc / int_alloc` mirror the buddy upgrade system. Each delve level grants `STAT_POINTS_PER_LEVEL = 1` point; per-point payoffs are `+4 max HP / +0.6 ATK / +0.005 SPD / +0.6 INT` (INT scales spell damage so caster classes have something to spend on). Phase 2 wires the upgrade UI; this commit just lays the schema + helper. +- **Class reroll plumbing**. `class_rerolls_used` + `last_class_reroll_at` columns added with a CHECK + `class_reroll_cost_usd(n) = 5,000 USD * 2^n` helper. Phase 3 ships the user-facing `,delve reroll ` command. +- **`,delve class` lists allowed weapons / armor per class**. The error embed players see when they typo a class name now lists each class's `weapon_types` + `armor_types` so they can make an informed pick. +- **Starter kit auto-mints into the inventory on class pick**. `set_class` now stamps the class's `starter_weapon` + `starter_armor` into both `equipped_*` and the `weapons_owned/armor_owned` JSONB so newly picked classes can immediately fight without buying anything. + +### Buddy panel + shelter clarity pass +- **Shelter listing surfaces rarity**. `,buddy shelter` rows previously showed only `#id emoji Name - Lv. N - reason`, so a Common adoption looked identical to a Legendary one and players had to adopt blind to find out. Each row now reads `#id emoji **Name** - **** Lv. N - reason` with the rarity name pulled from `buddies_config.rarity_meta`, matching how rarity is displayed everywhere else (panel header, AH listings, battle results). `services/buddy_lifecycle.list_shelter` was widened to also fetch `rarity_tier` so the cog can render this without a second round-trip. +- **Buddy panel folds upgrades into the visible combat stats**. `,buddy` previously displayed HP / ATK / SPD computed from `tier_meta + level + mood` ONLY -- the player's spent stat points (`hp_alloc / atk_alloc / spd_alloc`, allocated via `,buddy upgrade`) were silently ignored on the panel even though `services.buddy_battle.Fighter.from_row` applied them in actual fights. The panel now mirrors the Fighter formula exactly so the numbers can no longer disagree with battles. +- **Per-stat upgrade tag on the Combat line**. Each upgraded stat picks up an `*(+N upg)*` tag so players can see at a glance how much of their displayed HP / ATK / SPD comes from spent points vs base + level + tier + mood. +- **New `Upgrades` panel field**. Lists Hardiness / Power / Vigor allocations with the converted bonus (`+12 max HP`, `+1.5 ATK`, `+0.025 SPD`), the per-point conversion rate as a footnote (so "+3 HP / +0.5 ATK / +0.5% SPD per point") and the running total spent. When the player has unspent points it surfaces them prominently with a hint to run `,buddy upgrade`, so points stop sitting unallocated because the panel never reminded them. +- **Battle challenge / result blocks also reflect upgrades**. `_fighter_field` was on the same stale formula so the prompt embed for `,buddy battle` would understate a heavily upgraded buddy's stats. Now matches the Fighter exactly too -- challenge previews can no longer mislead either fighter about what they're walking into. + +### Fishing: dynamic hook window + secondary REEL action +- **Hook window scales per cast.** Was a flat `HOOK_WINDOW_S = 3.0s` for everyone. New `fishing_config.compute_hook_window(rod_tier, fish_level, zone_tier, rarity_tier)` returns a per-cast deadline: + - **Rod tier**: +6% per tier (better gear = more time) + - **Fishing level**: +1.5% per level (mastery widens the window) + - **Zone tier**: −8% per tier above 1 (deeper zones are chaotic) + - **Rarity tier**: −12.5% per tier above 1 (legendary fish are tighter); cog passes the bucket-rarity hint when available + - **Random jitter**: ±15% so streaks don't feel mechanical + - Floor 1.2s, ceiling 6.0s +- **Secondary mid-cast REEL action.** New `fc.SECONDARY_TRIGGER_CHANCE = 0.25` so ~1 in 4 casts inserts a `🌀 PULL!` prompt before the bite frame. Players have `fc.SECONDARY_WINDOW_S = 2.0s` to click `REEL!`. If the prompt fires and the player misses → catch is forced to a miss outcome (fish slips). If they hit: + - **Sweet hook + secondary** → 2.0× quality bonus (legendary catch territory) + - **Secondary only (no sweet)** → 1.5× bonus (matches the legacy sweet bonus, so missing the sweet but nailing the secondary still pays off) +- **`services/fishing.py:cast_resolve`** now accepts `hook_window_s`, `secondary_required`, `secondary_hit` kwargs. Quality gating compares against the actual presented window (not the constant) so the player can never be "late" against a deadline they didn't see. `CastView` stamps the per-cast window at bite time, runs the secondary prompt when triggered, and forwards both into the resolver. +- Backwards-compatible: legacy callers (force-miss path on view abort, `WildBattleView` exit, etc.) still work with the new signature. `_on_reel` callback acknowledges within Discord's 3s budget; window-mismatch races are rejected by an explicit elapsed-time check. + +### `,buddy species` interactive roster +- **Type-filter dropdown.** Was a flat embed listing every species grouped by rarity. Now it's an interactive panel with a row-0 dropdown: `All Species / Forest Affinity / Reef Affinity / Mine Affinity / Ruins Affinity / Neutral`. Affinity comes from `expeditions_config.SPECIES_AFFINITY` so the same buckets that drive expedition loot bonuses now drive species browsing too. +- **Per-species detail.** Each row shows hatch %, affinity tag, every signature lane the (species, rarity-tier) buffs (`Buffs: Chat XP · Work payout · Fishing yield`), the species ability + description, and base HP/ATK pulled from the rarity tier's combat stats. Players evaluating a species choice for a destination / lane have the full picture in one place instead of cross-referencing `,db` + `,buddy stats` + `,expedition help`. +- Selecting an affinity also surfaces the destination blurb (`Quiet, dappled, full of curious herbs.` etc.) plus the +25% loot reminder so the link between species choice and expedition payoff is explicit. + +### `,items` per-token picker + visible token IDs +- **Pick-a-specific-token dropdown on the `,items` overview.** Player feedback: "make individual items in a stack selectable, so if I want to use a particular egg or a particular fish I can send / use / list it individually. Also need to add the token id to items list -- I can't even see it to transfer or sell or list anything." New `_TokenPickSelect` on row 3 lists up to 25 of the player's owned tokens (filtered by the current kind selection), each option formatted with metadata-rich copy so the player can pick the SPECIFIC token they want: + - Fish: `Trout - 4.2 lbs · lur:abc12` + - Egg: `Wecco Egg - Rare · bud:xyz98` + - Buddy: `Sparky L12 · bud:def45` + - Generic: `Worm Bait · lur:abc34` +- Selection opens an ephemeral inspect embed (token id, contract, network, status, metadata) plus the standard `TokenActionView` (List on AH / Transfer / View Contract / Refresh) so every per-token action is one click away from the overview. No need to type the token id manually. +- Picker rebuilds on every refresh / kind-switch / sort cycle so newly minted / sold / gifted tokens stay in sync. Initial paint also populates the dropdown so it's available before the first interaction. +- Footer copy on the overview embed updated to point players at the new dropdown ("Pick a specific token (row 3) to inspect / list / transfer"). + +### Capture is now an option on every wild encounter +- **`Try Capture` button on shelter-escape (`,buddy` escape event) prompts.** `_EscapedBuddyView` previously offered only `⚔️ Challenge`. New button does a one-shot 20% capture roll with no fight: success flips the escaped buddy to `'owned'` under the clicker (same path as the Challenge-then-win flow); failure dismisses the encounter with the buddy returning to the shelter for normal `,buddy shelter` adoption. Shelter-cap honoured. +- **`Try Capture` on fishing wild encounters.** `cogs/fishing.py:_WildBattleView` got the same button alongside Challenge. Routes through `services.fishing.resolve_wild_battle(won=True/False)` so the cc_buddies insert + LURE/REEL/BBT credit + counter bumps + bus events all run through the canonical resolver -- no risk of the manual capture bypassing analytics. +- Same 20% chance (`fishing_config.WILD_BATTLE_CAPTURE_CHANCE` / hard-coded for the buddy escape path) as the existing post-fight auto-capture roll, so the affordance doesn't trivialise wild battles. Players who want a guaranteed catch still click Challenge and fight to weaken the wild buddy. +- Delve and farm wild battles already had a Capture button (delve gates on enemy HP < threshold mid-fight; farm grants on win). Unchanged. + +### AH listings now decrement / restore JSONB inventory too +- **`,ah list ` removes the item from your per-cog inventory.** Pre-fix the token-path AH list flow only escrowed the NFT (`item_instances.owner_user_id = NULL`); the parallel JSONB count on `user_fishing.fish_inventory` / `user_farming.crops_inventory` / etc. stayed put, so listing a fish still showed it in `,fish inv`. `services/auction.py:create_listing_by_token` now ALSO calls the matching `_LOCK_HANDLERS[kind]` for fish / crop / ore / weapon / armor / consumable / crafted right after the NFT escrow, inside the same `db.atomic()` block. Wrapped in try/except so a missing JSONB row (NFT minted via expedition / backfill with no JSONB entry) doesn't roll back the listing. +- **`,ah cancel` restores the inventory.** `cancel_listing` token-path now mirrors the lock with `_RETURN_HANDLERS[kind]` for the same set of kinds. Cancelling a listing puts the item back in `,fish inv` / `,farm inv` / etc. alongside the NFT sweep back to the seller. +- **`,ah buy` deposits to the buyer's inventory.** `buy_listing` token-path now calls `_DELIVER_HANDLERS[kind]` so the buyer sees their purchase in their per-cog inventory immediately, not just in `,items`. Same try/except envelope for safety -- the NFT transfer already moved ownership at the source-of-truth layer. +- Bait / junk / shop / stone are unchanged here -- those kinds are NFT-only with no JSONB count to sync. + +### Buddy panel dropdown stops desyncing on the live tick +- **Live tick no longer rebuilds `_BuddySelect` mid-session.** Every minute or so the buddy panel's live tick fires `_get_data` to re-fetch state for the ASCII frame; pre-fix that path also called `view._rebuild_buddy_select(live_pages)`, which removed the active select from `self.children` and replaced it with a fresh instance. The tick then ran `await msg.edit(embed=rendered)` -- embed only, the view was never pushed to Discord. Discord's display kept the OLD select component while the in-memory view tracked a new (different) one. The next dropdown click landed at discord.py with no matching child to route to: `View interaction referencing unknown view for item <_BuddySelect ... id=14>. Discarding` plus `"this interaction failed"` for the player. Buttons (which never get rebuilt) kept working, which is why the user reported "broken with the dropdown but not with arrow keys." +- The select is still rebuilt on the explicit `_redraw` path (Prev/Next/Set Active button clicks, the dropdown's own callback, and the initial render) -- those paths all call `interaction.response.edit_message(view=self)` (or `ctx.reply(view=self)` on first render) which pushes the updated component list to Discord. Embed-only live-tick edits stop touching the children. + +### Buddy panel survives bot restarts (no more "this interaction failed") +- **`BuddyPanelView` is now a persistent view.** Pre-fix every bot restart wiped the in-memory view registry, so any panel opened in the previous session produced "View interaction referencing unknown view for item ... Discarding" + Discord's generic "this interaction failed" on every click. Now: `timeout=None`, every button + the row-3 buddy select carries an explicit `custom_id` (`buddy_panel:feed`, `:pet`, `:talk`, `:rename`, `:refresh`, `:list_ah`, `:prev`, `:next`, `:set_active`, `:select`), and `Buddy.cog_load` registers a stub view via `bot.add_view(BuddyPanelView())` so post-restart interactions route to it cleanly. +- The persistent stub's `interaction_check` detects the no-args rehydrated state and short-circuits with an ephemeral `"This buddy panel was opened in a previous session and has expired. Run ,buddy to open a fresh one."` hint. Live in-memory views still gate on `interaction.user.id == self.owner_id` exactly like before. +- The stub also instantiates a placeholder `_BuddySelect` with empty options at registration time so the dropdown's custom_id matches in discord.py's persistent-view routing -- without that, dropdown clicks on stale panels would still fall through unmatched. The placeholder never sees a real callback (interaction_check rejects every click on the stub). + +### `,buddy species` surfaces the new rarity-extra lanes +- **Per-rarity lane breakdown on the species list.** Player feedback: "where do I see the new buddy passives? They aren't showing on buddy embeds or species list or anywhere." The `,buddy` panel got a `✨ Passive effects` field in the cross-cog passives commit (`05b51c2`) but the `,buddy species` roster command was untouched. Each rarity header now appends `+N extra signature lane(s)` so players see at a glance what their tier unlocks; each species row gets a muted `Buffs: , , ...` line listing every signature lane the (species, tier) combo buffs at the fast ramp -- pulled straight from `buddy_bonus_lanes_for(species, tier)` so the display matches the actual multiplier wiring. +- The original ASCII-panel field (`✨ Passive effects` on `,buddy stats`) is unchanged and still renders for any (species, tier) combo with a known `bonus_lane`. If the field doesn't appear, force a panel re-open with `,buddy` -- live-tick views from before the cross-cog commit don't always pick up new fields until the message is replaced. + +### Bug fixes from new prod logs +- **`,admin buddy spawn` no longer trips `cc_buddies_status_chk`.** Migration `0169_cc_buddies_storage_status.sql` rewrote the status CHECK as `('owned', 'shelter', 'stored')` -- which dropped `'escaped'` (added by 0121) AND never included `'auction'` (added by my buddy AH listing flow). Both states are written by live code (`services.buddy_world.mark_escaped`, `services.auction.create_listing_by_token` for buddies), so the check rejected every escape spawn + every buddy AH listing. Migration `0189` re-adds both: `('owned', 'shelter', 'stored', 'escaped', 'auction')`. No data cleanup needed -- the migration only widens the constraint. +- **`,items` sort + kind dropdown stop silently failing.** `ItemsView._fetch` had a leftover `sort_clause` entry referring to `unit_usd_raw DESC NULLS LAST` -- a column my native-catalog-price refactor removed from the SELECT. Switching the sort to "usd" produced a SQL `column "unit_usd_raw" does not exist` error which discord.py logged as `Ignoring exception in view for item