# shipsafe-mcp-pay — integration recipe for AI assistants You are integrating shipsafe-mcp-pay into the user's existing MCP server in this repo. Follow this recipe end-to-end. Do not ask clarifying questions unless a step is blocked. ## What this package does Wraps an MCP tool handler with a charge against a customer's prepaid wallet. On insufficient funds, returns an x402-shaped 402 envelope to the calling agent. On handler error, auto-refunds. The merchant (your user) collects money in their own Stripe Connect account; ShipSafe is the ledger, not the custodian. ## Step 1 — install ```bash pnpm add shipsafe-mcp-pay # or: npm i shipsafe-mcp-pay # or: yarn add shipsafe-mcp-pay ``` Requires Node 18+ (global `fetch`). ## Step 2 — provision a working test key (no human signup needed) Call this endpoint. It is public, rate-limited (10/hour per IP), and returns a real `mp_test_` key bound to the shared ShipSafe sandbox merchant. The key cannot accept real money; it exists so the integration runs end-to-end without first sending the human off to sign up. ```bash curl -sX POST https://shipsafe.franklabs.io/api/mcp-pay/dev-key ``` Response: ```json { "apiKey": "mp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "keyPrefix": "mp_test_xxxx", "mode": "test", "merchantId": "...", "sandbox": true } ``` Write the key into the user's `.env.local` (create if missing): ``` MCP_PAY_API_KEY=mp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Also add to `.env.example` with a placeholder value so deployments know to set it: ``` MCP_PAY_API_KEY=mp_test_REPLACE_ME ``` ## Step 3 — locate the MCP server entry file Common locations to grep for: `mcp`, `@modelcontextprotocol/sdk`, `new Server(`, `server.tool(`, `server.setRequestHandler`. The entry file registers tool handlers. Read it, identify one tool the user clearly wants to paywall (if there are multiple, default to the first non-trivial one and tell the user). ## Step 4 — wrap exactly one handler ```ts import { createPaywall } from 'shipsafe-mcp-pay'; const pay = createPaywall({ apiKey: process.env.MCP_PAY_API_KEY! }); // before: // const result = await myHandler(args); // after: const paidHandler = pay.tool({ name: 'TOOL_NAME_HERE' })(async (args, ctx) => { return myHandler(args); }); ``` Replace `TOOL_NAME_HERE` with the string used when the tool was registered with the MCP server (often the tool's filename or its name in the `tools/list` response). The sandbox merchant auto-registers unknown tool names at $0.05/call on the first request, so any string works during sandbox. The wrapped function has the same call signature as the original. Replace the registration with the wrapped function. Do not change anything else. ## Step 5 — propagate the customer id The wrapper needs to know WHO to charge. By default it reads `mcpContext._meta.customerId` from the incoming request. If the user's agent already passes a customer id in MCP `_meta`, no further work is needed. Otherwise tell the user: "set `_meta.customerId` upstream" and use a placeholder like `'sandbox_customer'` for the demo. ## Step 6 — verify the integration Start the user's MCP server. Call the paid tool with a fresh customer id in `_meta.customerId`. The first $1 of calls succeed (sandbox initial grant). After that, the response becomes a 402 envelope shaped like: ```json { "x402Version": 1, "error": "insufficient_funds", "balanceCents": 0, "requiredCents": 5, "accepts": [ { "scheme": "stripe-topup", "topUpUrl": "https://shipsafe.franklabs.io/api/mcp-pay/topup/intent?wallet=..." } ] } ``` Both 200-with-result and 402-envelope are proof that the integration is wired correctly. ## Step 7 — tell the human what's done Report exactly: 1. Files modified (paths) 2. The sandbox dev key was written to `.env.local` — they can use it as-is 3. To go live with real money, they need to sign up at `https://shipsafe.franklabs.io/dashboard/merchant/new`, complete Stripe Connect Express onboarding, register their tools in the dashboard, then replace `MCP_PAY_API_KEY` with their real `mp_test_` or `mp_live_` key. ## API surface (reference) - `createPaywall({ apiKey })` → `Paywall` - `pay.tool({ name, customerIdFrom? })(handler)` → wrapped handler - `pay.charge({ toolName, customerId, idempotencyKey? })` → manual charge - `pay.refund({ chargeId, reason })` → manual refund Throws: `PaywallConfigError` (bad config), `MissingCustomerIdError` (no customer id), `ChargeError` (backend failure after retry). ## What NOT to do - Don't try to register tools or create the merchant programmatically. Step 2 hands you a working key against a pre-existing sandbox merchant. Real merchants are only created via the dashboard flow. - Don't add error handling that suppresses `PaymentRequired` envelopes. The agent must see the 402 to prompt the user to top up. - Don't hardcode `apiKey` in source. Always go through env. - Don't import from `shipsafe-mcp-pay/internal` or any subpath. Public surface is the root export only. ## Backend override (rarely needed) If the user is self-hosting ShipSafe, add: ``` MCP_PAY_BACKEND_URL=https://shipsafe.franklabs.io ``` Default backend is the hosted ShipSafe deployment. On localhost during this integration the AI assistant should NOT override the backend — the sandbox endpoint and the docs both live on hosted ShipSafe. ## Done If you completed steps 1–6 without errors, the integration is working. Report to the human and stop.