Skip to main content

Overview

Batch payments let your users pre-pay a larger amount once and draw down across multiple tool calls. You configure a minimum upfront charge and continue to require a per-call price; the middleware handles balance tracking so users are not prompted on every call.

When to use batch payments

  • If your tool is called repeatedly in a workflow and per-call prompts are disruptive
  • If you want to reduce payment-approval friction while keeping per-call pricing
  • If you want to amortize network fees over several calls
Pick a minimum payment that covers several calls (e.g., 10× your tool’s per-call price) to reduce prompts without overcharging.

How it works

  1. You set minimumPayment in the atxpExpress middleware.
  2. Each tool still calls requirePayment({ price }) for its per-call charge.
  3. On the first call, the user pays the larger of minimumPayment and price.
  4. Subsequent calls deduct price from the remaining balance until depleted.

Prerequisites

  • ATXP account and connection string
  • Existing Express-based MCP server using the ATXP Express SDK.
1

Install dependencies

npm install @atxp/express bignumber.js
2

Configure server with minimumPayment

Add minimumPayment to your ATXP middleware and keep per-call pricing with requirePayment.
server.ts
import express from 'express'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { atxpExpress, requirePayment, ATXPPaymentDestination } from '@atxp/express'
import BigNumber from 'bignumber.js'
import { z } from 'zod'

const app = express()
app.use(express.json())

const ATXP_CONNECTION = process.env.ATXP_CONNECTION

// Require an upfront prepayment of $0.50 USDC (or higher if per-call price is larger)
app.use(
  atxpExpress({
    paymentDestination: new ATXPPaymentDestination(ATXP_CONNECTION),
    payeeName: 'My Batch-Paid Tool',
    minimumPayment: new BigNumber(0.50),
  })
)

const server = new McpServer({ name: 'my-batch-server', version: '1.0.0' })
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })

// Charge $0.05 USDC per call; draws down from the prepaid balance
server.tool(
  'process_text',
  'Process a text string with batch-paid pricing',
  { text: z.string().describe('Text to process') },
  async ({ text }) => {
    await requirePayment({ price: new BigNumber(0.05) })
    return {
      content: [{ type: 'text', text: text.toUpperCase() }],
    }
  }
)

const setupServer = async () => {
  await server.connect(transport)
}

app.post('/', async (req, res) => {
  try {
    await transport.handleRequest(req, res, req.body)
  } catch (err) {
    if (!res.headersSent) res.status(500).json({ error: 'Internal server error' })
  }
})

const PORT = process.env.PORT || 3000
setupServer().then(() => app.listen(PORT))
Ensure minimumPayment is a BigNumber instance. If you pass a number, amounts may be imprecise.
3

Client behavior and approval

The first tool call will request the larger amount. You can implement approval logic to auto‑approve reasonable prepayments.
client.ts
import { atxpClient } from '@atxp/client'
import { Account } from '@atxp/common'

const account = new Account(process.env.ATXP_CONNECTION_STRING!)
const client = await atxpClient({
  mcpServer: 'https://your-server.example.com',
  account,
  onPayment: async (pmt) => {
    console.log(`Payment succeeded: ${pmt.amount} ${pmt.currency}`)
  },
  onPaymentFailure: async (err) => {
    console.error('Payment failed', err)
  },
})

const result = await client.callTool({
  name: 'process_text',
  arguments: { text: 'hello world' },
})
To persist balances across restarts or scale-out, configure an OAuth database in atxpExpress (SQLite or Redis).
4

Verify batch behavior

  • First call prompts for the larger minimumPayment (e.g., $0.50)
  • Next several calls are not prompted until the prepaid balance is depleted
  • Once depleted, the next call will prompt again for at least minimumPayment
You should see only a single payment approval across multiple calls, then another prompt once the balance runs out.
I