ShipSafe
Vibe-coder installPaste this into Claude Code, Cursor, or Codex. Your AI will do the rest.
Add shipsafe-mcp-pay to this MCP server. Read https://shipsafe.franklabs.io/docs/mcp-pay/llms.txt and follow it exactly.
Spec your AI will read: /docs/mcp-pay/llms.txtNo signup required — a sandbox key is provisioned automatically.

AdPulse-style payments: the hard way vs. mcp-pay

If you built AdPulse-style payments the hard way, you wrote a payments company while trying to ship an ad audit tool. Here is the same code with shipsafe-mcp-pay.

Context

AdPulse (https://adpulse.fyi) is an MCP server with tools for ad campaign auditing, copy generation, competitor intel, and budget optimization. The dev wired x402 directly: per-tool pricing dict, on-chain transaction verification, manual refunds when a tool errored mid-flight, hand-rolled 402 envelopes. "I was building a payments company" was the quote.

Here is what that code looked like, and what it collapses to.

Before: hand-rolled x402, ~50 lines per tool

// adpulse/src/server.ts (excerpt)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { verifyX402Settlement, settlePayment } from './payments/x402.js';
import { recordLedgerEntry, refundLedgerEntry } from './payments/ledger.js';
import { auditCampaign } from './tools/audit.js';

const TOOL_PRICES_USDC: Record<string, number> = {
  audit_campaign: 0.25,
  generate_copy: 0.10,
  competitor_intel: 0.50,
  optimize_budget: 0.35,
};

const PAY_TO_ADDRESS = process.env.X402_PAYOUT_ADDRESS!;
const NETWORK = (process.env.X402_NETWORK ?? 'base') as 'base' | 'base-sepolia';

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const name = request.params.name;
  const args = request.params.arguments ?? {};
  const price = TOOL_PRICES_USDC[name];
  if (price == null) {
    return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
  }

  // x402 handshake: check for a settlement header on the incoming request.
  const settlement = request.params._meta?.['x-payment-settlement'] as string | undefined;
  if (!settlement) {
    return {
      isError: true,
      content: [{ type: 'text', text: 'Payment required' }],
      _meta: {
        'x-payment-required': 'x402; version=1',
        payment_required: {
          x402Version: 1,
          accepts: [{
            scheme: 'x402-usdc',
            network: NETWORK,
            payTo: PAY_TO_ADDRESS,
            amount: { value: price, currency: 'USDC' },
          }],
          error: 'payment_required',
        },
      },
    };
  }

  // Verify the settlement on chain. This costs an RPC call per request.
  const verified = await verifyX402Settlement({
    txHash: settlement,
    expectedAmount: price,
    expectedNetwork: NETWORK,
    expectedPayTo: PAY_TO_ADDRESS,
  });
  if (!verified.ok) {
    return { isError: true, content: [{ type: 'text', text: `Settlement invalid: ${verified.reason}` }] };
  }

  // Record the ledger entry so we don't double-credit if the agent retries.
  const ledgerId = await recordLedgerEntry({
    toolName: name,
    txHash: settlement,
    amountUsdc: price,
    customerWallet: verified.fromAddress,
  });

  // Now run the tool. If it throws, we have to issue a refund manually.
  try {
    const result = await runTool(name, args);
    return { content: [{ type: 'text', text: JSON.stringify(result) }] };
  } catch (err) {
    await refundLedgerEntry(ledgerId, 'handler_error');
    // Refund on x402 means a chain transaction back to the customer. Hope your
    // payout wallet has gas. Hope the network is up. Hope the customer wallet
    // is still valid.
    await settlePayment.refund({
      to: verified.fromAddress,
      amountUsdc: price,
      network: NETWORK,
      reason: `tool ${name} failed: ${(err as Error).message}`,
    });
    throw err;
  }
});

Plus everything not shown: a payments/x402.ts module that verifies on-chain settlements, a payments/ledger.ts module for idempotency, environment variables for the payout wallet's private key, a cron job to reconcile pending settlements, and a Slack alert for when refunds fail because gas spiked.

That is a payments company.

After: ~10 lines with shipsafe-mcp-pay

// adpulse/src/server.ts (excerpt)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { createPaywall } from 'shipsafe-mcp-pay';
import { auditCampaign, generateCopy, competitorIntel, optimizeBudget } from './tools/index.js';

const pay = createPaywall({ apiKey: process.env.MCP_PAY_API_KEY! });

const paidAudit = pay.tool({ name: 'audit_campaign' })(auditCampaign);
const paidCopy = pay.tool({ name: 'generate_copy' })(generateCopy);
const paidIntel = pay.tool({ name: 'competitor_intel' })(competitorIntel);
const paidOptimize = pay.tool({ name: 'optimize_budget' })(optimizeBudget);

const HANDLERS = { audit_campaign: paidAudit, generate_copy: paidCopy, competitor_intel: paidIntel, optimize_budget: paidOptimize } as const;

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const handler = HANDLERS[request.params.name as keyof typeof HANDLERS];
  if (!handler) return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }] };
  const mcpContext = { _meta: request.params._meta ?? {} };
  const result = await handler(request.params.arguments ?? {}, mcpContext);
  if ((result as any)?._meta?.payment_required) return result as any;
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});

That is it. Prices live in the ShipSafe dashboard, not the code. Top up once, draw down across many calls instead of one settlement per call. Both x402 and AP2 are advertised in the 402 response automatically.

What the library replaces

Everything in the "before" block, plus everything you did not write yet:

  • x402 handshake. Building, signing, and verifying the 402 envelope. mcp-pay builds it from your dashboard config; the backend verifies settlements.
  • Per-tool pricing dict. Prices live in the dashboard. Change them without redeploying.
  • On-chain settlement verification. ShipSafe verifies the USDC transaction on Base / Base-Sepolia. No RPC keys in your code.
  • Ledger and idempotency. source_ref dedup is built in. Network retries do not double-charge.
  • Refund logic. Handler throws? Wrapper auto-refunds against the chargeId. No on-chain refund transaction, no gas math, no payout-wallet management. The refund is a ledger entry, not a chain operation.
  • 402 envelope shape. MCP-compatible envelope with the right _meta.payment_required shape so MCP clients render the top-up flow.
  • AP2 compatibility. Same 402 also advertises ap2-cards (Stripe Checkout) for agents that prefer fiat. You did not have to think about it.
  • Status codes. Correct 200 / 402 / 500 semantics. Correct headers (X-Payment-Required).
  • Retry safety. One retry on 5xx with backoff, baked in.
  • Wallet model. Customers top up once, draw down across many calls. Reduces friction per request to zero after the first top-up.
  • Stripe Connect. Funds land in your Stripe account, not ShipSafe's. You own the money.

Want this wired in for free?

Email twells@gtsbahamas.com with your repo. I will read your existing payment code, open a PR that swaps it for mcp-pay, and you keep your Stripe account. No catch, no sales call.