Build a Telegram Payment Bot with VietQR & AgentPay

2026-10-16 · AgentPay VN

telegramvietqrpythonpaymentsagentpay

The Problem: Why Most Telegram Bots Can't Take Money

Imagine you've built a Telegram bot that sells digital courses. Every day, 50+ users ask "how do I pay?" You're answering manually, copying bank details, waiting for transfers, manually confirming each one. By week two, you're drowning—and half your customers abandon the flow before payment.

This is the #1 blocker for Vietnamese developers monetizing Telegram bots. Payment gateways either require complex merchant registration (Stripe, 2Checkout), don't support VietQR natively, or demand a cut for holding customer funds. You need something lightweight, instant, and trustless.

Enter AgentPay VN—an open-source Python SDK that lets your Telegram bot generate VietQR QR codes pointing straight to your bank account. No middleman. No fund-holding. Settlement confirms automatically via bank feeds.

What Is AgentPay VN?

AgentPay VN is an MIT-licensed toolkit with two layers:

  1. Python SDK (agentpay-vn pip package)—handles payment request creation, QR generation, and settlement confirmation.
  2. MCP Server (agentpay-mcp)—lets Claude and other AI agents orchestrate payments programmatically.

The 3-line payment flow: - create_payment_request() → generates a unique VietQR request - send_checkout_url() → delivers QR to the user (Telegram message, link, etc.) - await_settlement() → blocks until bank confirms the transfer

Why it's different: - ✅ No fund custody: QR points straight at merchant's bank. You keep every ₫. - ✅ Instant settlement: Bank feed confirms within seconds of transfer. - ✅ AI-native: MCP server means Claude can create payments, check status, refund—all without human intervention. - ✅ Open-source: Full transparency. Run on your own infrastructure.

Step 1: Install and Configure AgentPay

Installation

pip install agentpay-vn

Verify the install:

python -c "import agentpay; print(agentpay.__version__)"

Get Your Bank Credentials

AgentPay uses your actual bank account for settlement. You'll need:

  1. Bank account number (chính chủ, VietQR-compatible)
  2. Bank code (e.g., 970012 for Techcombank, 970022 for ACB)
  3. Account holder name (full legal name)

Create a .env file in your project:

AGENTPAY_BANK_ACCOUNT=1234567890
AGENTPAY_BANK_CODE=970012
AGENTPAY_MERCHANT_NAME="Nguyen Van A"
AGENTPAY_MERCHANT_ID="your-unique-id"
AGENTPAY_API_KEY="your-api-key"
TELEGRAM_BOT_TOKEN="your-telegram-token"

Initialize the SDK

import os
from agentpay import AgentPay
from dotenv import load_dotenv

load_dotenv()

# Initialize AgentPay client
client = AgentPay(
    bank_account=os.getenv("AGENTPAY_BANK_ACCOUNT"),
    bank_code=os.getenv("AGENTPAY_BANK_CODE"),
    merchant_name=os.getenv("AGENTPAY_MERCHANT_NAME"),
    merchant_id=os.getenv("AGENTPAY_MERCHANT_ID"),
    api_key=os.getenv("AGENTPAY_API_KEY")
)

print("✓ AgentPay initialized successfully")

Step 2: Build Your Telegram Bot Payment Handler

Let's create a bot that sells a course ($5 USD ≈ 127,000 ₫). When a user types /buy_course, the bot generates a payment request and sends a VietQR QR code.

import asyncio
import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes
from agentpay import AgentPay
import os
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Initialize clients
agentpay_client = AgentPay(
    bank_account=os.getenv("AGENTPAY_BANK_ACCOUNT"),
    bank_code=os.getenv("AGENTPAY_BANK_CODE"),
    merchant_name=os.getenv("AGENTPAY_MERCHANT_NAME"),
    merchant_id=os.getenv("AGENTPAY_MERCHANT_ID"),
    api_key=os.getenv("AGENTPAY_API_KEY")
)

async def buy_course(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Handles /buy_course command.
    Creates a payment request, generates QR, and sends to user.
    """
    user = update.effective_user
    course_price_vnd = 127000  # ~$5 USD
    transaction_id = f"course-{user.id}-{int(asyncio.get_event_loop().time())}"

    try:
        # Step 1: Create payment request via AgentPay
        payment_req = await agentpay_client.create_payment_request(
            amount=course_price_vnd,
            description=f"Python Course - User {user.id}",
            transaction_id=transaction_id,
            customer_name=user.first_name or "Customer",
            customer_phone="",  # Optional
            due_date=None  # No expiry
        )

        # Step 2: Get VietQR checkout URL and QR image
        checkout_url = payment_req.get("checkout_url")
        qr_image_url = payment_req.get("qr_image_url")

        logger.info(f"Payment request created: {transaction_id}")

        # Step 3: Send QR to user via Telegram
        keyboard = [
            [InlineKeyboardButton("🏦 Pay Now", url=checkout_url)],
            [InlineKeyboardButton("ℹ️ Instructions", callback_data="payment_help")]
        ]
        reply_markup = InlineKeyboardMarkup(keyboard)

        message = (
            f"🎓 *Python Mastery Course*\n\n"
            f"💰 Price: {course_price_vnd:,} ₫\n"
            f"📱 Scan the QR or click 'Pay Now'\n\n"
            f"_Transaction ID: {transaction_id}_"
        )

        # Send QR image
        if qr_image_url:
            await update.message.reply_photo(
                photo=qr_image_url,
                caption=message,
                reply_markup=reply_markup,
                parse_mode="Markdown"
            )
        else:
            await update.message.reply_text(
                message + f"\n\n[Pay here]({checkout_url})",
                reply_markup=reply_markup,
                parse_mode="Markdown"
            )

        # Step 4: Wait for settlement (runs in background)
        asyncio.create_task(
            wait_for_settlement(transaction_id, user.id, context)
        )

    except Exception as e:
        logger.error(f"Payment error: {e}")
        await update.message.reply_text(
            "❌ Payment setup failed. Please try again later."
        )

async def wait_for_settlement(
    transaction_id: str,
    user_id: int,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    """
    Polls AgentPay for settlement confirmation.
    Once confirmed, delivers course access to user.
    """
    try:
        # Block until payment is confirmed
        settlement = await agentpay_client.await_settlement(
            transaction_id=transaction_id,
            timeout_seconds=600  # Wait up to 10 minutes
        )

        if settlement.get("status") == "settled":
            logger.info(f"✓ Payment settled for {transaction_id}")

            # Grant course access
            # TODO: Update your database, send course link, etc.
            await context.bot.send_message(
                chat_id=user_id,
                text=(
                    "✅ *Payment Confirmed!*\n\n"
                    "Your course access is ready.\n"
                    "Download here: [Course Materials](https://your-course-link)\n\n"
                    "Happy learning! 🚀"
                ),
                parse_mode="Markdown"
            )
        else:
            await context.bot.send_message(
                chat_id=user_id,
                text="⏳ Payment pending. We'll notify you when confirmed."
            )

    except asyncio.TimeoutError:
        logger.warning(f"Settlement timeout for {transaction_id}")
        await context.bot.send_message(
            chat_id=user_id,
            text="⏱️ Payment check timed out. Please verify manually."
        )

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Start command."""
    await update.message.reply_text(
        "👋 Welcome to AgentPay Course Bot!\n"
        "Type /buy_course to purchase."
    )

async def main():
    # Create bot application
    application = Application.builder().token(
        os.getenv("TELEGRAM_BOT_TOKEN")
    ).build()

    # Add handlers
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("buy_course", buy_course))

    # Run bot
    await application.run_polling()

if __name__ == "__main__":
    asyncio.run(main())

Line-by-line breakdown:

Step 3: Integrate with Claude via MCP Server

Want Claude to handle payments autonomously? Use the MCP server.

Install MCP Server

pip install agentpay-mcp

Configure Claude to Use AgentPay Tools

Add this to your Claude configuration (e.g., claude_config.json):

{
  "mcp_servers": {
    "agentpay": {
      "command": "python",
      "args": ["-m", "agentpay_mcp"],
      "env": {
        "AGENTPAY_BANK_ACCOUNT": "1234567890",
        "AGENTPAY_BANK_CODE": "970012",
        "AGENTPAY_MERCHANT_NAME": "Nguyen Van A",
        "AGENTPAY_MERCHANT_ID": "your-id",
        "AGENTPAY_API_KEY": "your-key"
      }
    }
  }
}

Now Claude can: - Create payment requests - Check settlement status - Issue refunds - Query transaction history

Example Claude prompt:

"A customer wants to buy 3 courses at 127k each. Create a payment request, generate the QR, and wait for settlement. Then send them the download link."

Claude will automatically call agentpay.create_payment_request(), agentpay.await_settlement(), and your delivery logic.

Real-World Case Study: Café Bot

Let's say you run a café and want customers to pre-order via Telegram.

Workflow: 1. Customer: /menu 2. Bot shows items with prices (cappuccino 45k, pastry 30k, etc.) 3. Customer clicks "Add to Cart" → /checkout 4. Bot creates a payment request for the total (e.g., 75k) 5. Customer scans QR with their banking app, pays instantly 6. Bot detects settlement within 3–5 seconds 7. Bot sends order confirmation + pickup time

Code snippet:

async def checkout(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    cart = context.user_data.get("cart", [])
    total = sum(item["price"] for item in cart)

    payment = await agentpay_client.create_payment_request(
        amount=total,
        description=f"Café Order - {len(cart)} items",
        transaction_id=f"cafe-{update.effective_user.id}-{time()}"
    )

    await update.message.reply_text(
        f"Total: {total:,} ₫\nScan to pay:",
        photo=payment["qr_image_url"]
    )

Best Practices & Do's/Don'ts

✅ DO ❌ DON'T
Store transaction_id in database for audit trail Hardcode bank credentials in code
Use .env for sensitive data Create multiple payment requests for the same order
Set a reasonable timeout (5–10 min) for settlement Assume payment is settled without await_settlement()
Log all payment events for debugging Expose API keys in Git commits
Handle exceptions (network, timeout, invalid amount) Manually confirm payments—trust the bank feed
Test with real amounts on testnet first Process refunds without verifying settlement first

Advanced Tips

1. Webhook-Based Settlement (No Polling)

Instead of await_settlement() blocking, configure AgentPay to send webhooks:

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhook/agentpay", methods=["POST"])
def agentpay_webhook():
    data = request.json
    transaction_id = data.get("transaction_id")
    status = data.get("status")
    amount = data.get("amount")

    if status == "settled":
        # Deliver course/product immediately
        deliver_product(transaction_id)

    return {"ok": True}

2. Batch Refunds

Issue refunds programmatically:

await agentpay_client.create_refund(
    original_transaction_id="course-123-456",
    refund_amount=127000,
    reason="Customer requested cancellation"
)

3. Multi-Currency Pricing

Store prices in USD, convert at request time:

USD_TO_VND_RATE = 24500  # Update daily via API

def get_vnd_price(usd: float) -> int:
    return int(usd * USD_TO_VND_RATE)

# Usage
await agentpay_client.create_payment_request(
    amount=get_vnd_price(5.0),  # $5 = ~122,500 ₫
    description="Digital product",
    transaction_id=txn_id
)

FAQ

Q: Does AgentPay hold my money? A: No. The VietQR QR code points directly to your bank account. Money goes straight from the customer's bank to yours. AgentPay only monitors the transaction.

Q: How fast is settlement confirmation? A: Bank feeds update within 3–5 seconds. await_settlement() typically confirms within 10 seconds of a successful transfer.

Q: Can I use AgentPay for refunds? A: Yes. Call create_refund() with the original transaction ID. The SDK handles the reverse transfer to the customer's account.

Q: What happens if the customer closes the bot before paying? A: The payment request stays valid (unless you set an expiry). The customer can still scan the QR later. Your bot can retry settlement checks via background jobs or webhooks.

Key Takeaways

Get Started Now

  1. Install: pip install agentpay-vn
  2. Read the docs: https://agentpay.servicesai.vn/v1/docs
  3. Explore the code: https://github.com/phuocdu/agentpay-vn
  4. Build your bot: Copy the examples above and adapt to your use case.

Have questions? Open an issue on GitHub or join the AgentPay community. Happy building! 🚀

Get started →

← All posts