├── .gitignore ├── README.md ├── bot ├── commands │ ├── __init__.py │ ├── balance.py │ ├── refund.py │ └── start.py ├── handlers │ └── payment.py └── utils │ ├── __init__.py │ └── errors.py ├── config.py ├── files ├── example.png └── stars.svg ├── locales └── en │ └── messages.ftl ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | .venv/ 26 | env/ 27 | 28 | # IDE 29 | .vscode/ 30 | .idea/ 31 | *.swp 32 | *.swo 33 | *~ 34 | 35 | # Logs 36 | *.log 37 | 38 | # OS 39 | .DS_Store 40 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Telegram Stars Payment Bot Logo 3 | 4 |

Telegram Stars Payment Bot

5 | 6 |

7 | Process Telegram Stars payments, issue refunds, and monitor bot balance with localized error handling. 8 |

9 | 10 |

11 | Report Bug 12 | · 13 | Request Feature 14 | · 15 | Contact Author 16 |

17 |
18 | 19 | --- 20 | 21 | ## Features 22 | 23 | - Create Stars payment invoice via `/start` 24 | - Alternative: generate payment link (commented example in `payment.py`) 25 | - Perform refunds with `/refund ` 26 | - View bot Stars balance using `/balance` 27 | - Clear, structured refund error messages (Telegram charge status aware) 28 | 29 | ## Screenshot / Example 30 | 31 |
32 | Example Stars Payment Flow 33 |
34 | 35 | ## Quick Start 36 | 37 | ### 1. Clone & Install 38 | 39 | ```bash 40 | git clone https://github.com/bohd4nx/stars-payment.git 41 | cd stars-payment 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | ### 2. Configuration 46 | 47 | Edit `config.py`: 48 | 49 | ```python 50 | API_TOKEN = "your_bot_token_here" # Obtain at @BotFather 51 | ``` 52 | 53 | ### 3. Run 54 | 55 | ```bash 56 | python main.py 57 | ``` 58 | 59 | ## Commands 60 | 61 | | Command | Description | 62 | | ------------------------------------ | --------------------------------------- | 63 | | `/start` | Send a Stars payment invoice | 64 | | `/refund ` | Attempt refund of a prior Stars payment | 65 | | `/balance` | Show current Stars balance of the bot | 66 | 67 | --- 68 | 69 |
70 |

Made with ❤️ by @bohd4nx

71 |

Star ⭐ this repo if it helps your Telegram payments!

72 |
73 | -------------------------------------------------------------------------------- /bot/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .balance import router as balance_router 2 | from .refund import router as refund_router 3 | from .start import router as start_router 4 | 5 | __all__ = ['start_router', 'refund_router', 'balance_router'] 6 | -------------------------------------------------------------------------------- /bot/commands/balance.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, Bot 2 | from aiogram.filters import Command 3 | from aiogram.types import Message 4 | from aiogram_i18n import I18nContext 5 | 6 | router = Router(name=__name__) 7 | 8 | 9 | @router.message(Command("balance")) 10 | async def process_balance(message: Message, bot: Bot, i18n: I18nContext) -> None: 11 | balance = await bot.get_my_star_balance() 12 | me = await bot.me() 13 | await message.reply( 14 | i18n.get("balance-info", username=me.username, amount=balance.amount) 15 | ) 16 | -------------------------------------------------------------------------------- /bot/commands/refund.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, Bot 2 | from aiogram.exceptions import TelegramAPIError 3 | from aiogram.filters import Command 4 | from aiogram.methods.refund_star_payment import RefundStarPayment 5 | from aiogram.types import Message 6 | from aiogram_i18n import I18nContext 7 | 8 | from bot.utils import get_error_message 9 | 10 | router = Router(name=__name__) 11 | 12 | 13 | @router.message(Command("refund")) 14 | async def process_refund(message: Message, bot: Bot, i18n: I18nContext) -> None: 15 | parts = message.text.split() 16 | user_id = None 17 | transaction_id = None 18 | 19 | if len(parts) != 3: 20 | await message.reply(i18n.get("refund-invalid")) 21 | return 22 | 23 | try: 24 | user_id, transaction_id = int(parts[1]), parts[2] 25 | result = await bot(RefundStarPayment( 26 | user_id=user_id, 27 | telegram_payment_charge_id=transaction_id 28 | )) 29 | 30 | if result: 31 | await message.reply(i18n.get("refund-success")) 32 | else: 33 | await message.reply(i18n.get("refund-error", error="Unknown error")) 34 | 35 | except ValueError: 36 | await message.reply(i18n.get("refund-invalid")) 37 | except TelegramAPIError as e: 38 | await message.reply(get_error_message(i18n, str(e), user_id, transaction_id)) 39 | -------------------------------------------------------------------------------- /bot/commands/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.filters import Command 3 | from aiogram.types import Message 4 | from aiogram_i18n import I18nContext 5 | 6 | router = Router(name=__name__) 7 | 8 | 9 | @router.message(Command("start")) 10 | async def start_intro(message: Message, i18n: I18nContext) -> None: 11 | await message.reply(i18n.get("start-intro")) 12 | -------------------------------------------------------------------------------- /bot/handlers/payment.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F, html, Bot 2 | from aiogram.exceptions import TelegramAPIError 3 | from aiogram.types import Message, LabeledPrice, PreCheckoutQuery 4 | from aiogram_i18n import I18nContext 5 | 6 | router = Router(name=__name__) 7 | 8 | 9 | @router.message(F.text.regexp(r"^\d{1,6}$")) 10 | async def handle_amount(message: Message, bot: Bot, i18n: I18nContext) -> None: 11 | """ 12 | Flow: 13 | 1. User sends a number 1–100000 (raw Stars units) 14 | 2. Validate range; build single LabeledPrice (Stars requires exactly one component) 15 | 3. Call send_invoice with currency 'XTR' and empty provider_token (rule for Stars) 16 | 4. Optionally (commented) build a payment link instead of direct invoice. 17 | 18 | Core send_invoice parameters (Stars context): 19 | chat_id: user chat id (invoice destination) 20 | title: localized product/invoice title (1–32 chars) 21 | description: localized description (1–255 chars) 22 | payload: internal tracking string (NOT shown to user) – embeds amount 23 | currency: must be 'XTR' for Telegram Stars 24 | prices: list[LabeledPrice] – MUST contain exactly ONE item for Stars 25 | provider_token: empty string for Stars; normal fiat providers supply real token 26 | start_parameter: deep-link tag; forwarded copies behavior differs if present 27 | 28 | Optional (reference only): business_connection_id, subscription_period, photo_* metadata. 29 | Ignored for Stars: tips, user data requirements, shipping, flexible pricing. 30 | 31 | Docs: sendInvoice API – https://core.telegram.org/bots/api#sendinvoice 32 | Stars Overview – https://telegram.org/blog/telegram-stars 33 | Stars Bot API – https://core.telegram.org/bots/payments-stars 34 | """ 35 | try: 36 | amount = int(message.text) 37 | except ValueError: 38 | await message.reply(i18n.get("amount-invalid")) 39 | return 40 | if not (1 <= amount <= 100000): 41 | await message.reply(i18n.get("amount-invalid")) 42 | return 43 | 44 | try: 45 | await bot.send_invoice( 46 | chat_id=message.chat.id, 47 | title=i18n.get("invoice-title"), 48 | description=i18n.get("invoice-description"), 49 | provider_token="", # Empty for Stars 50 | currency="XTR", # Stars currency code 51 | prices=[LabeledPrice(label=i18n.get("invoice-label"), amount=amount)], # Exactly one item 52 | start_parameter='stars-payment', 53 | payload=f'stars-payment-{amount}' # Internal tracking 54 | # Optional parameters for Stars payments: 55 | # business_connection_id=None, 56 | # subscription_period=None, # Must be 2592000 (30 days) if used 57 | # photo_url=None, 58 | # photo_size=None, 59 | # photo_width=None, 60 | # photo_height=None, 61 | ) 62 | 63 | # Alternative: create a payment link instead of direct invoice. 64 | # Uncomment block below to send a link instead. 65 | # try: 66 | # invoice_link = await bot.create_invoice_link( 67 | # title=i18n.get("invoice-title"), 68 | # description=i18n.get("invoice-description"), 69 | # payload=f'stars-payment-{amount}', 70 | # provider_token="", # Empty for Stars 71 | # currency="XTR", 72 | # prices=[LabeledPrice(label=i18n.get("invoice-label"), amount=amount)] 73 | # ) 74 | # await message.answer( 75 | # i18n.get("payment-link", link=html.quote(invoice_link)) 76 | # ) 77 | # except TelegramAPIError: 78 | # pass 79 | except TelegramAPIError: 80 | await message.reply(i18n.get("invoice-error")) 81 | 82 | 83 | @router.pre_checkout_query() 84 | async def handle_pre_checkout(pre_checkout_query: PreCheckoutQuery, bot: Bot) -> None: 85 | await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True) 86 | 87 | 88 | @router.message(F.successful_payment) 89 | async def handle_successful_payment(message: Message, i18n: I18nContext) -> None: 90 | info = message.successful_payment 91 | await message.reply( 92 | i18n.get( 93 | "payment-success", 94 | amount=info.total_amount, 95 | transaction_id=html.quote(info.telegram_payment_charge_id) 96 | ) 97 | ) 98 | -------------------------------------------------------------------------------- /bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import get_error_message 2 | 3 | __all__ = ['get_error_message'] 4 | -------------------------------------------------------------------------------- /bot/utils/errors.py: -------------------------------------------------------------------------------- 1 | ERROR_KEYS = { 2 | "CHARGE_ALREADY_REFUNDED": "charge-already-refunded", 3 | "CHARGE_NOT_FOUND": "charge-not-found", 4 | "REFUND_FAILED": "refund-failed", 5 | "DEFAULT": "refund-default" 6 | } 7 | 8 | 9 | def _format_tx_id(tx_id: str) -> str: 10 | return f"{tx_id[:6]}...{tx_id[-6:]}" if len(tx_id) > 12 else tx_id 11 | 12 | 13 | def get_error_message(i18n, error_text: str, user_id: int, transaction_id: str) -> str: 14 | upper = error_text.upper() if error_text else "" 15 | error_code = next((code for code in ERROR_KEYS if code in upper), "DEFAULT") 16 | key = ERROR_KEYS.get(error_code, ERROR_KEYS["DEFAULT"]) 17 | tx_short = _format_tx_id(transaction_id) if transaction_id else "-" 18 | return i18n.get( 19 | key, 20 | tx_short=tx_short, 21 | user_id=str(user_id), 22 | error=error_text 23 | ) 24 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | API_TOKEN: str = '' # Your bot token from @BotFather 2 | -------------------------------------------------------------------------------- /files/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bohd4nx/stars-payment/057cbcf6a018e157fcc26e57458295242cc9de11/files/example.png -------------------------------------------------------------------------------- /files/stars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/en/messages.ftl: -------------------------------------------------------------------------------- 1 | start-intro = 2 | ⭐️ Telegram Stars Payment Demo Bot 3 | 4 | This bot demonstrates payment, refund, and balance features with Telegram Stars. 5 | 6 | How to pay: send a number (1 – 100000) as a message and you will receive an invoice for that many ⭐️. 7 | 8 | Refund: Use /refund <user_id> <transaction_id> after a successful payment. 9 | Balance: Use /balance to view current Stars balance of the bot. 10 | 11 | Send an amount now to generate an invoice. 12 | 13 | refund-invalid = 14 | ❌ Please use format: /refund '<user_id>' '<transaction_id>' 15 | 16 | ℹ️ Example: /refund 123456789 ABC123XYZ 17 | 18 | refund-default = 19 | ❌ Refund failed 20 | 21 | 🆔 Transaction: { $tx_short } 22 | 👤 User ID: { $user_id } 23 | 24 | 💭 Error details: 25 |
{ $error }
26 | 27 | refund-failed = 28 | ❌ Refund failed 29 | 30 | 🆔 Transaction: { $tx_short } 31 | 👤 User ID: { $user_id } 32 | 33 | ⚠️ The bot may have insufficient balance or a Telegram-side error occurred. 34 | 35 | charge-already-refunded = 36 | 💰 Refund already processed 37 | 38 | 🆔 Transaction: { $tx_short } 39 | 👤 User ID: { $user_id } 40 | 41 | ℹ️ This payment has already been refunded. 42 | 43 | charge-not-found = 44 | ❓ Transaction not found 45 | 46 | 🆔 Transaction: { $tx_short } 47 | 👤 User ID: { $user_id } 48 | 49 | ⚠️ The specified transaction does not exist. 50 | 51 | payment-success = 52 | 🎉 Payment successful! 53 | 54 | 💵 Amount: { $amount }⭐️ 55 | 56 | 🆔 Transaction ID: { $transaction_id } 57 | 58 | refund-error = 59 | ❌ Failed to refund payment:
{ $error }
60 | 61 | amount-invalid = 62 | ❌ Invalid amount 63 | 64 | Please send a whole number between 1 and 100000. 65 | 66 | ℹ️ Example: 150 67 | 68 | balance-info = 69 | 💰 Bot (@{ $username }) balance: { $amount }⭐️ 70 | 71 | payment-link = Payment link: { $link } 72 | 73 | refund-success = ✅ Payment has been successfully refunded! 74 | 75 | invoice-description = Payment for services via Stars. 76 | 77 | invoice-error = ❌ Failed to create payment invoice 78 | 79 | invoice-label = Stars Payment 80 | 81 | invoice-title = Stars Payment Example 82 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, Dispatcher 5 | from aiogram.client.default import DefaultBotProperties 6 | from aiogram.enums import ParseMode 7 | from aiogram_i18n import I18nMiddleware 8 | from aiogram_i18n.cores.fluent_runtime_core import FluentRuntimeCore 9 | 10 | from bot.commands import start_router 11 | from bot.commands.balance import router as balance_router 12 | from bot.commands.refund import router as refund_router 13 | from bot.handlers.payment import router as payment_handlers_router 14 | from config import API_TOKEN 15 | 16 | logging.basicConfig( 17 | level=logging.ERROR, 18 | format='[%(asctime)s] - %(levelname)s: %(message)s', 19 | datefmt='%H:%M:%S' 20 | ) 21 | dispatcher_logger = logging.getLogger('aiogram.dispatcher') 22 | dispatcher_logger.setLevel(logging.INFO) 23 | 24 | bot = Bot( 25 | token=API_TOKEN, 26 | default=DefaultBotProperties( 27 | parse_mode=ParseMode.HTML, 28 | link_preview_is_disabled=True 29 | ) 30 | ) 31 | dp = Dispatcher() 32 | 33 | i18n_middleware = I18nMiddleware( 34 | core=FluentRuntimeCore(path="locales/{locale}"), 35 | default_locale="en" 36 | ) 37 | 38 | dp.include_router(start_router) 39 | dp.include_router(payment_handlers_router) 40 | dp.include_router(balance_router) 41 | dp.include_router(refund_router) 42 | 43 | dp.update.middleware(i18n_middleware) 44 | dp.message.middleware(i18n_middleware) 45 | dp.callback_query.middleware(i18n_middleware) 46 | 47 | i18n_middleware.setup(dispatcher=dp) 48 | 49 | 50 | async def main(): 51 | await dp.start_polling(bot, skip_updates=True) 52 | 53 | 54 | if __name__ == "__main__": 55 | asyncio.run(main()) 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bohd4nx/stars-payment/057cbcf6a018e157fcc26e57458295242cc9de11/requirements.txt --------------------------------------------------------------------------------