shipsafe-mcp-pay
v0.1.0
Published
MCP payments middleware — paywall your tools in 5 lines. AP2 + x402 compatible.
Maintainers
Readme
shipsafe-mcp-pay
MCP payments middleware. Wrap a tool handler, declare a price, charge per call. AP2 + x402 compatible. Zero runtime deps.
Install
pnpm add shipsafe-mcp-payRequires Node 18+ (uses global fetch).
Quickstart
import { createPaywall } from 'shipsafe-mcp-pay';
const pay = createPaywall({ apiKey: process.env.MCP_PAY_KEY! });
const paidWeather = pay.tool({ name: 'weather' })(async (args, ctx) => {
// ctx.customerId, ctx.walletId, ctx.balanceCents, ctx.chargeId
return { temperature: 72, city: args.city };
});
// Register `paidWeather` with your MCP server as a tool handler.On every call, the wrapper:
- Extracts the customer id from the incoming MCP request.
- POSTs
/api/mcp-pay/chargewith the tool name and customer id. - If
200, runs your handler. If the handler throws, automatically refunds. - If
402, returns an MCP-compatible payment-required envelope to the agent.
AP2 + x402 compatibility
mcp-pay speaks both major agent-payment standards from one config surface.
| Standard | Scheme tag | When it fires |
|---|---|---|
| Anthropic AP2 | ap2-cards | Hosted Stripe Checkout — best for agents already account-attached. |
| Coinbase x402 (HTTP 402) | x402-usdc | USDC settlement on Base / Base-Sepolia for arbitrary callers. |
| Direct Stripe top-up | stripe-topup | Same Stripe Checkout URL exposed as a fallback. |
Every 402 response sets the header:
X-Payment-Required: x402; version=1, ap2; version=0.2…and the body includes every scheme the merchant has configured. Callers pick the one their wallet supports.
API
createPaywall(opts) → Paywall
type PaywallOptions = {
apiKey: string; // mp_test_... or mp_live_...
backendUrl?: string; // default: hosted ShipSafe deployment
fetch?: typeof fetch; // injection point for tests
onChargeError?: (err: unknown) => void;
};Throws PaywallConfigError if the key is missing or has the wrong prefix.
pay.tool(opts)(handler)
Wraps an async MCP tool handler. The returned function is what you register with your MCP server.
type ToolOptions = {
name: string;
customerIdFrom?:
| 'context' // default — mcpContext._meta.customerId
| 'header' // mcpContext._meta.headers['x-customer-id']
| ((req: any) => string); // custom extractor
};Your handler receives (args, ctx) where ctx is the post-debit ToolContext:
type ToolContext = {
customerId: string;
walletId: string;
balanceCents: number; // AFTER the debit
chargeId: string; // for refund tracking
};If the handler throws, mcp-pay issues a refund against ctx.chargeId and re-throws the original error.
pay.charge(opts) → ChargeResult
Direct charge — useful for tests or custom flows that don't go through a tool wrapper.
const result = await pay.charge({
toolName: 'weather',
customerId: 'cus_abc',
idempotencyKey: 'req_123',
});
if (result.ok) {
// result.balanceCents, result.chargeId, result.walletId
} else {
// result.status === 402, result.body is the PaymentRequiredBody
}pay.refund(opts) → RefundResult
await pay.refund({ chargeId: 'ch_1', reason: 'handler_error' });build402Response(input)
Lower-level helper to build the 402 envelope yourself (e.g. for HTTP-only paywalls outside MCP).
import { build402Response } from 'shipsafe-mcp-pay';
const { status, headers, body } = build402Response({
priceCents: 25,
stripeCheckoutUrl: 'https://checkout.stripe.com/pay/abc',
usdcPayTo: '0xabc...',
usdcNetwork: 'base',
});Errors and status codes
| Error class | When |
|---|---|
| PaywallConfigError | apiKey missing or wrong prefix, missing tool({name}). |
| MissingCustomerIdError | Could not extract a customer identifier from the MCP request. |
| Plain Error (charge/refund) | Backend returned 4xx (non-402) or 5xx after one retry. |
Network errors and 5xx responses are retried once with a 150ms backoff before surfacing.
Docs
- Protocol overview: shipsafe.io/docs/mcp-pay (forthcoming)
- Companion:
@shipsafe/mcp— ShipSafe's own MCP server, gated by this package.
Documentation
Full guides in docs/mcp-pay/:
- Quickstart — 5-minute onboarding from install to first paid call.
- Architecture — request flow, AP2/x402 compatibility, Stripe Connect, wallet model, failure modes.
- AdPulse example — hand-rolled x402 (~50 lines) vs. mcp-pay (~10 lines).
- API reference — every option, every type, every error class.
License
MIT.
