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
- You set
minimumPayment in the atxpExpress middleware.
- Each tool still calls
requirePayment({ price }) for its per-call charge.
- On the first call, the user pays the larger of
minimumPayment and price.
- 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.
Install dependencies
npm install @atxp/express bignumber.js
Configure server with minimumPayment
Add minimumPayment to your ATXP middleware and keep per-call pricing with requirePayment.import express from 'express'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { atxpExpress, requirePayment, ATXPAccount } 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({
destination: new ATXPAccount(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.
Client behavior and approval
The first tool call will request the larger amount. You can implement approval logic to auto‑approve reasonable prepayments.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).
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.