Add shipsafe-mcp-pay to this MCP server. Read https://shipsafe.franklabs.io/docs/mcp-pay/llms.txt and follow it exactly.Architecture: how shipsafe-mcp-pay works
How a paid tool call flows from agent to wallet ledger, and what happens when things go sideways.
Request flow

The wrapper sits between your MCP server and your handler. It:
- Pulls
customerIdout of the incoming MCP request (_meta.customerIdby default). - POSTs
/api/mcp-pay/chargewith{ toolName, customerId }. - On
200, runs your handler with actxobject holding the post-debit balance and ledger id. - On
402, returns an MCP-shaped payment-required envelope. Your handler never runs. - If your handler throws, calls
/api/mcp-pay/refundwith thechargeIdand reasonhandler_error, then re-throws the original error.
The package has zero runtime dependencies. The only network call is to the ShipSafe backend.
AP2 vs. x402 compatibility
mcp-pay speaks both major agent-payment standards from one config surface. The merchant configures which schemes are enabled in the dashboard. The 402 response advertises all of them; the caller picks one.
| Standard | Scheme tag | Settlement | When it fires |
|---|---|---|---|
| Anthropic AP2 | ap2-cards |
Hosted Stripe Checkout | Agents already account-attached to a Stripe customer. |
| Coinbase x402 (HTTP 402) | x402-usdc |
USDC on Base or Base-Sepolia | Arbitrary callers with a crypto wallet. |
| Direct Stripe top-up | stripe-topup |
Hosted Stripe Checkout | Same Checkout URL exposed as a fallback for non-AP2 agents. |
Every 402 response sets the header:
X-Payment-Required: x402; version=1, ap2; version=0.2And the body is an x402-v1 envelope:
{
"x402Version": 1,
"accepts": [
{
"scheme": "ap2-cards",
"checkoutUrl": "https://checkout.stripe.com/pay/cs_...",
"amount": { "value": 5, "currency": "USD" }
},
{
"scheme": "x402-usdc",
"network": "base",
"payTo": "0xabc...",
"amount": { "value": 0.05, "currency": "USDC" }
},
{
"scheme": "stripe-topup",
"topUpUrl": "https://checkout.stripe.com/pay/cs_...",
"amount": { "value": 5, "currency": "USD" }
}
],
"error": "insufficient_funds",
"message": "Wallet balance insufficient for this tool"
}MCP clients see the same data wrapped in an MCP-compatible envelope:
{
"isError": true,
"content": [{ "type": "text", "text": "Payment required..." }],
"_meta": {
"payment_required": { /* the body above */ },
"x-payment-required": "x402; version=1, ap2; version=0.2"
}
}Stripe Connect Express: where the money lives
Each merchant has a Stripe Connect Express account, created during onboarding at /merchants/new. When a customer tops up a wallet:
- Customer pays via the merchant's hosted Stripe Checkout URL.
- Funds land in the merchant's Stripe Connect account (not ShipSafe's).
- ShipSafe credits the customer's wallet in the ledger.
- Per-call debits draw down the ledger balance; settled funds remain in the merchant's Stripe account.
ShipSafe is the ledger and rails, not the custodian. Money sits with the merchant.
Funds settle into the merchant's Stripe Connect Express account, never ShipSafe's. If ShipSafe disappears tomorrow, the merchant's money is still in their Stripe account. The wallet ledger is a routing layer, not a custody layer.
Wallet model
The wallets table is keyed by (merchant_id, customer_id), with balance_cents as the running balance:
| Column | Type | Notes |
|---|---|---|
merchant_id |
uuid | Composite key |
customer_id |
text | Composite key |
balance_cents |
integer | Running balance after debits and top-ups |
A wallet is keyed by (merchant_id, customer_id). Funds are isolated by merchant: a top-up against merchant A's Stripe account credits merchant A's wallet for that customer only. The same customer with merchant B has a separate wallet, separate balance.
balanceCents returned in ToolContext is the wallet balance after the debit.
Idempotency: source_ref dedup
Every charge writes a ledger_entries row with a source_ref. The default source_ref is ${toolName}:${customerId}:${requestId} where requestId is the MCP request id. If you pass idempotencyKey to pay.charge, that overrides the default.
If two charges arrive with the same source_ref, the backend returns the original chargeId and balance without double-debiting. This protects against:
- Network retries from the MCP host.
- Duplicate MCP calls when the agent re-sends a request.
- Retries inside
mcp-payitself (one retry on 5xx, see below).
Failure modes and fallbacks
Insufficient funds
Backend returns 402 with error: "insufficient_funds". The wrapper converts to an MCP envelope and returns to the agent. Your handler does not run. Nothing is debited.
Network failure to ShipSafe backend
The wrapper retries once with a 150ms backoff. If both attempts fail:
pay.charge(used insidepay.tool) throws.onChargeError(if configured) is called with the error.- The handler does not run. No debit.
5xx from ShipSafe backend
Same as network failure. One retry, then throw.
Handler throws after successful charge
The wrapper calls /api/mcp-pay/refund with { chargeId, reason: 'handler_error' }, then re-throws the original handler error. The customer is not charged for a failed handler.
If the refund call itself fails, onChargeError is invoked with the refund error. The original handler error still propagates. The refund attempt is best-effort; for hard guarantees, run a reconciliation job against the ledger.
Malformed responses
If the backend returns 200 but with missing fields, or 402 with a non-x402-v1 body, the wrapper throws a ChargeError. Treat this as a backend bug, not a payment failure.
Customer id missing
If the wrapper cannot extract a customerId from mcpContext, it throws MissingCustomerIdError before making any network call. No debit, no risk. Fix by setting _meta.customerId upstream or passing customerIdFrom: 'header' / a custom function.
What lives where
| Concern | Location |
|---|---|
| Per-tool price | ShipSafe dashboard (merchant settings) |
| API key | Your env (MCP_PAY_API_KEY) |
| Customer wallet balance | ShipSafe Postgres (wallets table) |
| Ledger entries (charges, refunds, top-ups) | ShipSafe Postgres (ledger_entries) |
| Settled funds | Merchant's Stripe Connect Express account |
| Customer identity | Your application (you tell mcp-pay the id) |
The package itself is stateless. All state lives server-side in the ShipSafe backend or in your merchant Stripe account.