Build a Paid MCP Server: Charge Users Inside Claude

2026-09-25 · AgentPay VN

mcpclaudepaymentspythonvietqr

The Problem: AI Agents Need Revenue Models

You've built an amazing MCP server that helps users—maybe it writes better code, researches market trends, or automates customer support. Your Claude plugin works flawlessly. But here's the catch: users expect it for free, and you're footing the API bills.

Traditional payment gateways won't help. Stripe requires account setup, webhook complexity, and 2.9% + $0.30 per transaction. Worse, payment flows break Claude's conversational flow—users get bounced to external checkout pages, lose context, and abandon halfway through.

What if payments happened inside the conversation? What if your AI agent could ask for payment, the user scans a QR code on their phone, and—within seconds—the agent confirms payment and delivers the result? That's the AgentPay VN promise.

What Is AgentPay VN?

AgentPay VN is an open-source (MIT license) Python SDK + MCP server that lets AI agents collect VietQR payments directly from conversations. The key innovation: AgentPay never touches your money. Instead, it generates QR codes that point straight to your merchant bank account. When a user scans and pays, your bank confirms the settlement via API.

This means: - Zero payment custody risk - No separate merchant account setup beyond your existing bank - Instant settlement confirmation in Claude - Built for Vietnamese banking infrastructure, but extensible

Install in one line:

pip install agentpay-vn

Then plug the MCP server into Claude, and you're ready to charge.

The 3-Line Payment Flow

Before diving into code, understand AgentPay's core workflow:

  1. create_payment_request → Generate a unique payment request (amount, description, reference ID)
  2. send checkout_url → Share the QR code/checkout link with the user
  3. await_settlement → Poll your bank feed until the payment confirms

That's it. No OAuth redirects, no webhook spaghetti, no third-party account management.

Step 1: Set Up Your AgentPay Environment

Start by installing the SDK and configuring your bank credentials.

Installation

pip install agentpay-vn

Next, you'll need: - Bank Account: A Vietnamese business or personal bank account that supports VietQR (most major banks do: Vietcombank, Agribank, Techcombank, etc.) - Bank API Access: Credentials to read your bank statement or transaction feed. Most banks offer this via internet banking or an open-banking API. - MCP Integration: The AgentPay MCP server package

Installing the MCP Server

For Claude Desktop integration:

pip install agentpay-mcp

Or build from source:

git clone https://github.com/phuocdu/agentpay-vn.git
cd agentpay-vn
pip install -e .

Step 2: Create Your First Payment Request

Here's a real Python example—imagine you're building a "code review bot" that charges $5 per review:

from agentpay_vn import PaymentClient, create_payment_request
import asyncio

# Initialize the payment client with your bank details
client = PaymentClient(
    merchant_id="your_merchant_id",  # Unique ID for your account
    bank_account="1234567890",       # Your VietQR-enabled bank account
    bank_code="970422",               # Vietcombank's code; see docs for others
    api_key="your_bank_api_key"       # From your bank's developer portal
)

# Create a payment request for a code review
request = create_payment_request(
    amount=5.00,                           # USD equivalent (AgentPay handles conversion)
    currency="USD",
    description="Code Review: refactor_user_auth.py",
    reference_id="review_user_auth_001",  # Unique per transaction
    metadata={"user_id": "claude_user_42", "service": "code_review"}
)

print(f"Payment Request ID: {request.id}")
print(f"QR Code URL: {request.qr_code_url}")
print(f"Amount (VND): {request.amount_vnd}")

Line-by-line breakdown: - PaymentClient holds your bank credentials securely - create_payment_request() generates a unique payment object with an expiry (default 15 minutes) - amount is in your preferred currency; AgentPay converts to VND using live rates - reference_id ties the payment to your internal system (store this!) - qr_code_url points to a QR that users scan with any banking app

Step 3: Integrate with Claude via MCP

Now the magic: make this payment flow available inside Claude conversations.

Configure the MCP Server in Claude Desktop

Edit ~/.claude/config.json (create if missing):

{
  "mcpServers": {
    "agentpay": {
      "command": "python",
      "args": ["-m", "agentpay_mcp.server"],
      "env": {
        "AGENTPAY_MERCHANT_ID": "your_merchant_id",
        "AGENTPAY_BANK_ACCOUNT": "1234567890",
        "AGENTPAY_BANK_CODE": "970422",
        "AGENTPAY_API_KEY": "your_bank_api_key"
      }
    }
  }
}

Restart Claude Desktop. You'll now have access to AgentPay tools within conversations.

Step 4: Poll for Settlement

Once the user pays, you need to confirm it. Here's how to wait for settlement:

async def wait_for_payment(client, reference_id, timeout_seconds=120):
    """
    Poll the bank feed until the payment is confirmed.
    Returns True if payment received, False if timeout.
    """
    start_time = asyncio.get_event_loop().time()

    while asyncio.get_event_loop().time() - start_time < timeout_seconds:
        # Check your bank's transaction feed
        transactions = await client.fetch_bank_transactions(
            limit=10,
            filters={"reference_id": reference_id}
        )

        # Look for a matching payment
        for txn in transactions:
            if txn.reference_id == reference_id and txn.status == "SETTLED":
                print(f"✅ Payment confirmed! Transaction ID: {txn.id}")
                print(f"Amount received: {txn.amount_vnd} VND")
                return True

        # Wait 2 seconds before checking again (reduces API calls)
        await asyncio.sleep(2)

    print(f"❌ Timeout waiting for payment after {timeout_seconds}s")
    return False

# Usage in your MCP handler:
async def handle_code_review_request(code, user_id):
    # Create payment request
    payment = create_payment_request(
        amount=5.00,
        description=f"Code Review for {user_id}",
        reference_id=f"review_{user_id}_{int(time.time())}"
    )

    # Tell Claude to show the QR to the user
    yield f"Please scan this QR code to pay: {payment.qr_code_url}"

    # Wait for settlement
    is_paid = await wait_for_payment(client, payment.reference_id, timeout_seconds=120)

    if is_paid:
        # Deliver the service
        review = await perform_code_review(code)
        yield f"Your code review:\n\n{review}"
    else:
        yield "Payment not received. Please try again."

Key points: - fetch_bank_transactions() queries your actual bank feed (via their API) - Polling every 2 seconds is efficient; most settlements happen within 5-10 seconds - The reference_id ties everything together—payment request → transaction confirmation → service delivery

Real-World Example: A Paid Course Bot

Let's build a concrete scenario: an "AI prompt engineering course" bot that charges $9.99 per lesson.

The Bot Workflow

  1. User asks Claude: "I want to learn advanced prompt engineering. Show me lesson 3."
  2. Claude's MCP server checks if the user has paid for lesson 3
  3. If not: "Lesson 3 costs $9.99. Scan this QR code to unlock it: [QR link]"
  4. User scans on their phone (takes 5 seconds), confirms payment in their banking app
  5. Bot detects settlement: "Payment confirmed! Here's lesson 3..." [delivers full lesson content]
  6. Bot logs the transaction for your records (user_id → lesson_id → payment_id)

Code Sketch

from agentpay_vn import PaymentClient
import json

class CourseBot:
    def __init__(self, client):
        self.client = client
        self.paid_lessons = {}  # Track paid lessons per user

    async def unlock_lesson(self, user_id, lesson_id):
        # Check if already paid
        if self.is_user_paid(user_id, lesson_id):
            return await self.get_lesson_content(lesson_id)

        # Create payment request
        lesson_price = 9.99
        payment = create_payment_request(
            amount=lesson_price,
            currency="USD",
            description=f"Prompt Engineering Course - Lesson {lesson_id}",
            reference_id=f"{user_id}_lesson_{lesson_id}_{int(time.time())}",
            metadata={"user_id": user_id, "lesson_id": lesson_id}
        )

        # Prompt user to pay
        yield f"Lesson {lesson_id} costs ${lesson_price}. Scan to pay: {payment.qr_code_url}"

        # Wait for payment
        is_paid = await self.client.await_settlement(
            reference_id=payment.reference_id,
            timeout=120
        )

        if is_paid:
            # Mark as paid and deliver
            self.paid_lessons[user_id] = self.paid_lessons.get(user_id, set())
            self.paid_lessons[user_id].add(lesson_id)
            yield await self.get_lesson_content(lesson_id)
        else:
            yield "Payment failed. Try again?"

    def is_user_paid(self, user_id, lesson_id):
        return lesson_id in self.paid_lessons.get(user_id, set())

    async def get_lesson_content(self, lesson_id):
        # Return the actual lesson (e.g., from a database)
        return f"[Full content of Lesson {lesson_id}...]"

Do's and Don'ts: Payment Best Practices

✅ DO ❌ DON'T
Store reference_id in your database for reconciliation Charge without showing the QR code first
Set reasonable timeout (60–120 seconds) Timeout instantly; users need time to scan
Log all transactions (payment_id, user_id, status) Forget to log failures for debugging
Use unique reference_ids (avoid duplicates) Reuse reference_ids across transactions
Handle network errors gracefully (retry logic) Crash if bank API is slow
Offer payment retry if settlement times out Block user permanently after one failed attempt

Advanced: Handling Edge Cases

Multiple Retries with Exponential Backoff

import random

async def await_settlement_with_retry(client, reference_id, max_retries=3):
    for attempt in range(max_retries):
        try:
            result = await client.await_settlement(
                reference_id=reference_id,
                timeout=120
            )
            if result:
                return True
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt < max_retries - 1:
                # Exponential backoff: 1s, 2s, 4s
                wait_time = 2 ** attempt + random.uniform(0, 1)
                await asyncio.sleep(wait_time)
    return False

Currency Conversion

AgentPay auto-converts USD ↔ VND using live rates. For other currencies:

payment = create_payment_request(
    amount=100,  # 100 EUR
    currency="EUR",
    # AgentPay converts EUR → VND at current rate
)

Frequently Asked Questions

Q: Does AgentPay hold my money? No. QR codes point directly to your merchant bank account. Settlement happens bank-to-bank via VietQR. AgentPay only facilitates the payment flow.

Q: What if a user pays but I deliver the service, then the transaction reverses? Implement a webhook listener (advanced) or periodic reconciliation. Store transaction IDs from bank confirmations. If a reversal occurs, you can disable the user's access to that service. See docs for webhook setup.

Q: Can I charge in USD/EUR if I'm in Vietnam? Yes. AgentPay converts currencies automatically. However, your bank may apply FX fees. Check your bank's rate.

Q: How long does settlement take? Most VietQR payments settle within 5–30 seconds. Some banks take up to 2 minutes during peak hours. Set timeout to 120 seconds for safety.

Q: Can I use this outside Vietnam? Currently, AgentPay targets Vietnamese banking (VietQR). International support is on the roadmap. Join the GitHub discussions for updates.

Key Takeaways

Next Steps

  1. Get started now: Install AgentPay VN with pip install agentpay-vn
  2. Read the docs: Full API reference at https://agentpay.servicesai.vn/v1/docs
  3. Explore the repo: Check out examples and contribute at https://github.com/phuocdu/agentpay-vn
  4. Build your bot: Start with a simple paid service (e.g., $1 for a feature), test the flow, then scale

Your AI agent just became a revenue engine. Happy selling!

Get started →

← All posts