Sell Digital Products from AI Chatbots Without Holding Funds

2026-07-22 · AgentPay VN

ai-paymentsvietqrpython-sdkdigital-productsmcp-server

The Problem: Building Payment-Enabled AI Agents Is a Compliance Nightmare

Imagine you've built a brilliant AI chatbot that sells online courses, e-books, or software licenses. Your users love it. They're ready to pay. But now you're facing a wall of problems:

This is where AgentPay VN changes the game.

What Is AgentPay VN? A Zero-Custody Payment Layer for AI

AgentPay VN is an open-source (MIT license) Python SDK and MCP server that lets AI agents generate VietQR payment requests that point directly at the merchant's bank account. No middleman. No fund holding. No compliance theater.

Here's the core principle: when your chatbot creates a payment request, it generates a QR code that directs the customer to pay the merchant's actual bank account—not a holding account. The merchant's bank feed then confirms settlement in real-time. Your AI agent is the orchestrator, not the custodian.

Key facts: - Open-source under MIT license (you own your integration) - Lightweight Python SDK: pip install agentpay-vn - MCP server for Claude and other AI frameworks - Supports VietQR (Vietnam's interbank QR standard) - Zero fund holding—payments go straight to merchant banks - Bank feed integration for instant settlement confirmation

Why This Matters: The Three-Line Payment Flow

Traditional payment APIs require you to understand state machines, webhook handling, and reconciliation logic. AgentPay VN collapses payment collection into three lines:

# 1. Create a payment request
payment = await agent.create_payment_request(
    amount=500_000,  # VND
    description="E-book: Python for AI Agents",
    merchant_id="your_merchant_id"
)

# 2. Send the checkout URL to the user
await chatbot.send_message(f"Scan here: {payment.checkout_url}")

# 3. Wait for settlement confirmation
await agent.await_settlement(payment.request_id)

That's it. No webhooks to debug. No reconciliation loops. The payment request contains everything needed: amount, merchant bank details, and a unique reference ID. The user scans, pays, and your bank feed confirms—all asynchronously.

Setting Up AgentPay VN: A Step-by-Step Guide

Step 1: Install the SDK

pip install agentpay-vn

This installs the core Python library. It's lightweight—no heavy dependencies.

Step 2: Configure Your Merchant Account

You'll need: - A Vietnamese business bank account (or your merchant's account) - Your bank's API credentials for the feed integration - A merchant ID (provided by your bank or AgentPay partner)

Store these securely in environment variables:

export AGENTPAY_MERCHANT_ID="your_merchant_id"
export AGENTPAY_BANK_ACCOUNT="0123456789"
export AGENTPAY_BANK_CODE="VCCB"  # Vietcombank example

Step 3: Initialize the Agent

from agentpay_vn import AgentPayClient
import asyncio

# Create the client
client = AgentPayClient(
    merchant_id=os.getenv("AGENTPAY_MERCHANT_ID"),
    bank_account=os.getenv("AGENTPAY_BANK_ACCOUNT"),
    bank_code=os.getenv("AGENTPAY_BANK_CODE")
)

# Verify connection
await client.health_check()
print("AgentPay VN is ready!")

Step 4: Add to Your Chatbot

If you're using Claude with MCP, register the AgentPay MCP server:

{
  "mcpServers": {
    "agentpay-mcp": {
      "command": "agentpay-mcp",
      "args": [],
      "env": {
        "AGENTPAY_MERCHANT_ID": "your_merchant_id",
        "AGENTPAY_BANK_ACCOUNT": "0123456789",
        "AGENTPAY_BANK_CODE": "VCCB"
      }
    }
  }
}

Once registered, Claude can call AgentPay functions directly. No extra code needed in your main bot.

Real-World Walkthrough: Building a Course-Selling Chatbot

Let's build a concrete example: a chatbot that sells Python courses.

The scenario: - User asks: "I want to buy the Advanced Python course." - Bot responds with price and payment QR. - User scans and pays via mobile banking. - Bot detects settlement and delivers the course immediately.

The code:

import asyncio
from agentpay_vn import AgentPayClient

class CourseBot:
    def __init__(self):
        self.client = AgentPayClient(
            merchant_id="course_seller_123",
            bank_account="0123456789",
            bank_code="VCCB"
        )
        self.courses = {
            "python-advanced": {"price": 500_000, "title": "Advanced Python Mastery"},
            "django-pro": {"price": 750_000, "title": "Django for Enterprise"},
        }

    async def sell_course(self, user_id: str, course_id: str):
        """Execute a course sale end-to-end."""
        # Validate course
        if course_id not in self.courses:
            return {"error": "Course not found"}

        course = self.courses[course_id]

        # Create payment request
        payment = await self.client.create_payment_request(
            amount=course["price"],
            description=f"Course: {course['title']}",
            merchant_id="course_seller_123",
            metadata={"user_id": user_id, "course_id": course_id}
        )

        # Send checkout URL to user
        print(f"\n📲 Payment link for {user_id}:")
        print(f"Scan this: {payment.checkout_url}")
        print(f"Reference: {payment.request_id}\n")

        # Wait for settlement (non-blocking)
        try:
            settlement = await asyncio.wait_for(
                self.client.await_settlement(payment.request_id),
                timeout=3600  # Wait up to 1 hour
            )

            # Payment confirmed—deliver course
            print(f"✅ Payment confirmed! Delivering {course['title']}...")
            return {
                "status": "success",
                "course": course["title"],
                "download_link": f"https://yourcdn.com/courses/{course_id}/access.zip?user={user_id}"
            }

        except asyncio.TimeoutError:
            return {"status": "timeout", "request_id": payment.request_id}

    async def check_payment_status(self, request_id: str):
        """Manual check for a payment request."""
        return await self.client.get_payment_status(request_id)

# Usage
async def main():
    bot = CourseBot()
    result = await bot.sell_course("user_42", "python-advanced")
    print(result)

asyncio.run(main())

What happens step-by-step:

  1. create_payment_request() generates a unique VietQR payload with the merchant's bank details, amount, and reference ID.
  2. The QR code is displayed in the chat.
  3. User scans with any banking app and pays directly to the merchant's account.
  4. await_settlement() polls the merchant's bank feed (via API or webhook) for confirmation.
  5. Once confirmed, the bot delivers the course immediately.

Cost breakdown: Zero platform fees. Only standard bank transfer fees (typically free or ₫0–5,000).

Advanced Tips: Production Hardening

Tip 1: Handle Payment Timeouts Gracefully

Not all users complete payment instantly. Design for this:

async def sell_with_retry(self, user_id: str, course_id: str, max_attempts=3):
    """Allow user to re-check payment status."""
    payment = await self.client.create_payment_request(
        amount=self.courses[course_id]["price"],
        description=f"Course: {self.courses[course_id]['title']}"
    )

    print(f"Payment reference: {payment.request_id}")
    print(f"Check payment status anytime with: /status {payment.request_id}")

    # Non-blocking—user can come back later
    return payment.request_id

async def check_and_deliver(self, user_id: str, request_id: str):
    """User runs this later if payment wasn't detected."""
    status = await self.client.get_payment_status(request_id)
    if status["settled"]:
        # Deliver course
        print(f"✅ Payment confirmed. Delivering...")
    else:
        print(f"⏳ Still waiting. Amount received: {status['amount_received']} VND")

Tip 2: Idempotency for Webhook Duplicates

Bank feeds can fire duplicate confirmations. Make your settlement handler idempotent:

async def handle_settlement_webhook(self, request_id: str, amount: int):
    """Handle bank feed webhook safely."""
    # Check if already processed
    if await self.db.payment_delivered(request_id):
        return {"status": "already_delivered", "request_id": request_id}

    # Mark as delivered BEFORE external actions
    await self.db.mark_as_delivered(request_id)

    # Now deliver course (safe to retry if this fails)
    await self.deliver_course(request_id)

    return {"status": "delivered", "request_id": request_id}

Tip 3: Reconciliation Checks

Periodically verify your records against the bank:

async def reconcile(self):
    """Daily: check for orphaned payments."""
    pending_payments = await self.db.get_pending_payments()

    for payment in pending_payments:
        if payment.created_at < datetime.now() - timedelta(days=7):
            # Over a week old—check bank directly
            bank_status = await self.client.get_payment_status(payment.request_id)
            if bank_status["settled"]:
                await self.deliver_course(payment.request_id)
                print(f"✅ Reconciled: {payment.request_id}")

Comparison: AgentPay VN vs. Traditional Payment Processors

Feature AgentPay VN Stripe / PayPal Square
Fund holding None (direct to merchant) Yes (7-day reserve) Yes (varies)
Setup time Minutes Days/weeks Days
Regional focus Vietnam / VietQR Global US/Global
Fees Bank transfer only (₫0–5k) 2.9% + $0.30 per transaction 2.6% + $0.20
Compliance burden Minimal PCI-DSS (unless hosted) PCI-DSS
Open source Yes (MIT) No No
AI-agent native Yes (MCP server) No No
Settlement time Real-time (bank feed) 1–7 days 1–3 days

For a Vietnamese course seller making 50 sales @ ₫500,000 each: - AgentPay VN: ₫0–250,000 total (bank fees only) - Stripe: ₫7,250,000 (2.9% + $0.30 × 50)

Do's and Don'ts

Do: - ✅ Store the request_id in your database immediately after creating a payment request. - ✅ Use await_settlement() with a reasonable timeout (1–24 hours). - ✅ Enable bank feed webhooks for real-time settlement notifications. - ✅ Test with small amounts first (₫10,000–50,000). - ✅ Log all payment events for reconciliation.

Don't: - ❌ Deliver digital goods before settlement is confirmed. - ❌ Store customer bank details—AgentPay handles that. - ❌ Re-create payment requests for the same order (use get_payment_status() instead). - ❌ Assume payment succeeded just because the QR was scanned. - ❌ Ignore reconciliation—monthly audits prevent lost revenue.

FAQ

Q: What if a customer scans the QR but never pays?

A: The payment request expires after 24 hours (configurable). Your bot can remind the user or generate a fresh QR. Unpaid requests incur no fees.

Q: Can I use AgentPay VN for subscriptions?

A: Not directly—VietQR is designed for one-time transfers. For subscriptions, you'd need to regenerate payment requests monthly. We're exploring recurring payment support in a future release.

Q: What if the bank feed doesn't confirm settlement?

A: Implement a manual reconciliation flow. You can check the payment status API, or have users confirm payment with a screenshot. The await_settlement() function will time out gracefully.

Q: Is AgentPay PCI-DSS compliant?

A: Yes—but since you never touch customer bank details (they're handled by VietQR), you inherit the compliance of Vietnam's national payment system. No card data in your app = no PCI-DSS burden.

Key Takeaways

Whether you're building a course-selling bot, an e-book marketplace, or a SaaS sign-up flow, AgentPay VN removes the compliance friction and lets you focus on what matters: your AI product.

Get Started Now

Install AgentPay VN today:

pip install agentpay-vn

Set up the MCP server for Claude:

agentpay-mcp

Read the full documentation: https://agentpay.servicesai.vn/v1/docs

Explore the source code: https://github.com/phuocdu/agentpay-vn

Have questions? Join the community or open an issue on GitHub. Happy selling! 🚀

Get started →

← All posts