@seedhape/x402-merchant-sdk
v0.5.2
Published
Add x402 payment-gated routes, settlement receipts, and merchant accounting to Node.js services.
Maintainers
Readme
@seedhape/x402-merchant-sdk
One catalog for agent commerce
Use createMerchantCommerce to define products once. The same product
definitions can feed the SDK's current x402 paywall and future ACP/UCP/MPP
adapters; merchants do not maintain protocol-specific catalogs.
const commerce = createMerchantCommerce({
paywall: { payTo, ...base, facilitator: base.facilitator },
products: [{
id: "weather", name: "Weather API", description: "Current weather",
endpoint: "/weather",
price: { amount: 50_000n, ...base },
}],
});
commerce.catalog(); // publish to your agent-facing catalog
commerce.paywallFor("weather"); // pass to existing paywall middlewareThe helper normalizes discovery metadata and product lookup. Fulfillment and refunds remain merchant-owned and should use the purchase ID as their durable idempotency key.
Unified seller facade (preview)
createSeedhapeSeller is the single-drop-in entry point for a server-side
Express route. Define each product once and keep provider credentials in the
server environment:
import { base, createSeedhapeSeller } from "@seedhape/x402-merchant-sdk";
const seller = createSeedhapeSeller({
products: [{
id: "weather",
name: "Weather API",
description: "Current weather data",
endpoint: "/weather",
price: "0.50",
fulfill: async () => ({ temperature: 24 }),
}],
payments: {
x402: {
payTo: process.env.MERCHANT_WALLET!,
network: base.network,
asset: base.asset,
decimals: base.decimals,
facilitator: base.facilitator,
},
stripeMpp: {
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
profileId: process.env.STRIPE_PROFILE_ID!,
mppSecretKey: process.env.MPP_SECRET_KEY!,
},
},
});
app.all("/weather", seller.route("weather"));The facade preserves the existing x402 Bazaar discovery challenge. It selects Stripe MPP when a client advertises the MPP payment scheme and selects x402 otherwise. ACP/UCP checkout remains a separate integration gate; it will consume the same catalog rather than requiring another merchant catalog or middleware stack.
Add an x402 payment gate to an existing Express, Hono, or node:http service. The SDK returns a standard 402 Payment Required challenge, verifies the buyer's payment through an x402 facilitator, settles USDC directly to the merchant wallet, and exposes a receipt for your application ledger.
Install
npm install @seedhape/x402-merchant-sdkStripe machine payments (MPP)
Install the optional MPP runtime when you want agents to pay with Stripe cards or Shared Payment Tokens:
npm install mppxCreate one Stripe MPP paywall per seller endpoint. The seller keeps the Stripe
secret key and profile_test_... profile ID on the server; the SDK returns the
payment challenge and settles the credential without exposing either value:
import { createStripeMppPaywall } from "@seedhape/x402-merchant-sdk";
const mpp = await createStripeMppPaywall({
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
profileId: process.env.STRIPE_PROFILE_ID!,
mppSecretKey: process.env.MPP_SECRET_KEY!,
amount: "0.50",
currency: "usd",
description: "Weather API request",
});
export async function weather(request: Request) {
return mpp.handle(request, async () =>
Response.json({ temperature: 24, conditions: "clear" }),
);
}An unpaid agent receives HTTP 402; a valid credential is verified and
settled before the fulfillment callback runs. For a Stripe Connect direct
charge, pass connect: { stripeAccount: "acct_..." }. To enable Stripe's
stablecoin MPP rail as well, supply a Stripe SDK client and a test Tempo deposit
address. Use profile_test_... and sk_test_... in a Stripe Sandbox.
Express
import express from "express";
import { base, expressPaywall, merchantConfig } from "@seedhape/x402-merchant-sdk";
const app = express();
app.use(expressPaywall(merchantConfig(base, {
payTo: "0xYourBaseMerchantWallet",
paymentMethods: ["eip3009", "erc7710"],
price: ({ path }) => path === "/weather"
? { amount: 50_000n, ...base }
: null,
free: ["/health", "/docs/*"],
facilitator: { url: process.env.FACILITATOR_URL! },
route: {
name: "Weather API",
description: "Current weather data",
category: "weather",
inputSchema: { type: "object", properties: { city: { type: "string" } } },
outputSchema: { type: "object" },
},
receipts: { record: async (receipt) => db.receipts.insert(receipt) },
})));
app.get("/weather", (req, res) => {
res.json({ temperature: 24, conditions: "clear", receipt: req.x402Receipt?.id });
});price may be a fixed Money object or a resolver based on the request path, method, and query. Return null for a free route. USDC amounts are integer base units: 50_000n is 0.05 USDC.
Network presets and payment methods
Use base, baseSepolia, solana, or solanaDevnet to preconfigure the CAIP-2 network, USDC mint/contract, decimals, and default facilitator:
import { baseSepolia, expressPaywall, merchantConfig } from "@seedhape/x402-merchant-sdk";
app.use(expressPaywall(merchantConfig(baseSepolia, {
payTo: "0xYourStagingWallet",
paymentMethods: ["eip3009"],
price: ({ path }) => path === "/weather"
? { amount: 10_000n, ...baseSepolia }
: null,
})));paymentMethods defaults to ['eip3009'], the broadly supported payment method. Set ['eip3009', 'erc7710'] to let the buyer/client choose both methods. Only advertise ERC-7710 when the configured facilitator supports it. If the methods use different facilitators, configure facilitators: { eip3009: {...}, erc7710: {...} }; the SDK selects the facilitator matching the buyer's selected method.
Base and Solana on one route
Use accepts to expose both chains from the same resource. Bazaar and downstream catalogs such as Agentic Market treat these as payment choices on one service listing:
import { base, solana, expressPaywall, merchantConfig } from "@seedhape/x402-merchant-sdk";
// Create cdpFacilitator with createCdpFacilitatorClient as shown below.
app.use(expressPaywall(merchantConfig(base, {
accepts: [{
...base,
payTo: process.env.BASE_MERCHANT_WALLET!,
paymentMethods: ["eip3009"],
}, {
...solana,
payTo: process.env.SOLANA_MERCHANT_WALLET!,
feePayer: process.env.SOLANA_FEE_PAYER!,
}],
price: { amount: 10_000n, ...base },
facilitator: {
url: "https://api.cdp.coinbase.com/platform/v2/x402",
client: cdpFacilitator,
},
route: {
name: "Market Data",
description: "Paid market data on Base or Solana",
inputSchema: { type: "object", properties: { symbol: { type: "string" } } },
outputSchema: { type: "object" },
},
})));feePayer is the Solana facilitator signer advertised by its /supported response; it is not the merchant wallet. Use a facilitator that supports every advertised network, or set a per-option facilitator. Solana payments use the official x402 exact SVM scheme and a base64-encoded, partially signed versioned transaction in PAYMENT-SIGNATURE.
The built-in Solana mainnet preset uses solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp and native USDC mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Solana addresses are case-sensitive.
Its default facilitator is https://facilitator.payai.network; the devnet preset uses the test-only https://x402.org/facilitator.
Browser wallet paywall
Install the optional official UI when people should be able to open the paid route directly and pay with an EVM or Solana browser wallet:
npm install @seedhape/x402-merchant-sdk @x402/paywall react react-domThen enable it on the same configuration:
const config = merchantConfig(base, {
accepts: [
{ ...base, payTo: process.env.BASE_MERCHANT_WALLET! },
{
...solana,
payTo: process.env.SOLANA_MERCHANT_WALLET!,
feePayer: process.env.SOLANA_FEE_PAYER!,
},
],
price: { amount: 10_000n, ...base },
browserPaywall: {
appName: "Market Data",
appLogo: "https://api.example.com/icon-512.png",
testnet: false,
},
facilitator: { url: "https://facilitator.payai.network" },
});Express, Hono, and node:http adapters now return the official @x402/paywall HTML when the request accepts text/html. Programmatic callers continue receiving the normal JSON challenge and PAYMENT-REQUIRED header. The UI registers both official evmPaywall and svmPaywall handlers. Because @x402/paywall is an optional peer dependency, API-only installations do not pull its browser-wallet and React dependency tree.
Coinbase CDP Facilitator and Bazaar
For Coinbase Bazaar indexing, use Coinbase's authenticated facilitator client rather than passing the CDP API secret as a bearer token:
npm install @coinbase/cdp-sdkimport { createCdpFacilitatorClient } from "@coinbase/cdp-sdk/x402";
const cdpFacilitator = createCdpFacilitatorClient({
apiKeyId: process.env.CDP_API_KEY_ID!,
apiKeySecret: process.env.CDP_API_KEY_SECRET!,
});
const config = merchantConfig(base, {
payTo: process.env.MERCHANT_WALLET!,
facilitator: {
url: "https://api.cdp.coinbase.com/platform/v2/x402",
client: cdpFacilitator,
},
price: { amount: 10_000n, ...base },
route: {
name: "Link Lens URL Inspector",
description: "Extract metadata and readable text from a public URL.",
inputSchema: { type: "object", properties: { url: { type: "string", format: "uri" } }, required: ["url"] },
outputSchema: {
type: "object",
properties: { title: { type: "string" }, text: { type: "string" } },
required: ["title", "text"],
},
outputExample: { title: "Example page", text: "Readable page text" },
tags: ["web", "metadata", "extraction"],
iconUrl: "https://api.example.com/icon-512.png",
},
});Set CDP_API_KEY_ID and CDP_API_KEY_SECRET on the server only. The SDK uses the injected client for official EIP-3009 verification and settlement; the existing URL/API-key configuration remains available for other facilitators and custom ERC-7710 endpoints. After deployment, validate the endpoint and complete one successful payment through CDP for Bazaar indexing.
Do not treat FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402 by itself as CDP authentication, and do not send CDP_API_KEY_SECRET as a raw bearer token. Use createCdpFacilitatorClient() so the CDP SDK creates the required authenticated facilitator requests.
Bazaar publication checklist
Deploy the route at a public HTTPS URL.
Include route metadata and accurate input/output schemas so the SDK emits
extensions.bazaar.Validate the endpoint before paying:
curl -X POST https://api.cdp.coinbase.com/platform/v2/x402/validate \ -H "Content-Type: application/json" \ -d '{"resource":"https://api.example.com/report","method":"GET"}'Continue only when the response reports
valid: trueand an accepted simulation outcome.Complete one successful payment through the CDP Facilitator. There is no separate Bazaar registration form or publish API. CDP indexes the endpoint after settlement; indexing may initially report
processingand become searchable asynchronously.Confirm the facilitator extension response reports Bazaar
successorprocessing, then search the CDP Bazaar catalog for the resource.
The route metadata automatically becomes the Bazaar extensions.bazaar.info object and populates the resource serviceName and tags fields in the x402 v2 challenge. Use outputExample because agents use a realistic result example when deciding whether to call a service. Sonar can read the same resource URL, description, schemas, payment requirements, network, asset, and payment methods from that challenge.
iconUrl is copied into the resource and Bazaar metadata for downstream marketplaces such as Agentic Market. Use a publicly accessible HTTPS image URL, preferably a square PNG or WebP without authentication.
When deploying behind a TLS reverse proxy, set publicOrigin so the resource URL advertises the public HTTPS origin while preserving request paths and query parameters:
publicOrigin: "https://api.example.com",Hono and node:http
Use honoPaywall(config) with Hono or nodePaywall(config) for a raw Node server. All adapters use the same PaywallConfig and facilitator contract.
HTTP and MCP together
One merchant can expose the same service through both an HTTP API and an MCP tool. Reuse the network, USDC asset, recipient, facilitator, and receipt sink, then register a paid MCP tool with the official @x402/mcp wrapper:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { base, createPaidMcpTool, merchantConfig } from "@seedhape/x402-merchant-sdk";
const config = merchantConfig(base, {
payTo: process.env.MERCHANT_WALLET!,
price: { amount: 50_000n, ...base },
route: { name: "Market Data", description: "Paid market data" },
receipts: { record: async (receipt) => db.receipts.insert(receipt) },
});
const mcp = new McpServer({ name: "market-data", version: "1.0.0" });
await createPaidMcpTool(mcp, config, {
name: "get_market_data",
description: "Get paid market data",
inputSchema: { symbol: z.string() },
inputJsonSchema: {
type: "object",
properties: { symbol: { type: "string" } },
required: ["symbol"],
},
}, async ({ symbol }) => ({ symbol, price: 24812.05 }));The HTTP route and MCP tool produce separate x402 resources, so each can have its own price and discovery URL while sharing the same merchant wallet and facilitator.
Optional AP2 Checkout JWT
Enable AP2 on an HTTP paywall to issue a signed merchant Checkout JWT with every unpaid or rejected 402 response. The SDK returns it in the AP2-CHECKOUT-JWT header; this does not change x402 verification or settlement.
AP2 is optional. A route without the ap2 option behaves exactly like a normal x402 route. AP2 adds signed checkout context for AP2-aware buyers and agents; the facilitator still verifies and settles the EIP-3009 or ERC-7710 payment. The current Checkout JWT binds one network, so SDK routes using ap2 must configure a single payment option; multi-network Solana AP2 mandates are a separate follow-up.
app.use(expressPaywall(merchantConfig(base, {
payTo: process.env.MERCHANT_WALLET!,
price: { amount: 50_000n, ...base },
ap2: {
issuer: "https://api.example.com",
privateKeyPem: process.env.MERCHANT_AP2_PRIVATE_KEY_PEM!,
keyId: "merchant-ap2-1",
merchantId: "weather-api",
merchantName: "Example Weather",
merchantWebsite: "https://example.com",
expiresInSeconds: 300,
},
route: { name: "Weather API", description: "Current weather data" },
})));Store the configuration in the server environment:
MERCHANT_AP2_ISSUER=https://api.example.com
MERCHANT_AP2_KEY_ID=merchant-ap2-1
MERCHANT_AP2_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----"Generate an ES256 PKCS#8 key with OpenSSL:
openssl ecparam -name prime256v1 -genkey -noout -out ap2-private.pem
openssl pkcs8 -topk8 -nocrypt -in ap2-private.pem -out ap2-private-pkcs8.pemLoad the PEM as one environment variable, replacing line breaks with \\n, or load it from your deployment secret manager. Never expose the private key in browser code, a client bundle, or a public repository.
The JWT includes the merchant, resource, product, price, currency, payout address, network, issuer, issue time, and expiry. It is returned only on unpaid or rejected 402 responses:
HTTP/1.1 402 Payment Required
AP2-CHECKOUT-JWT: eyJ...
PAYMENT-REQUIRED: eyJ...createAp2CheckoutJwt() is also exported for custom transports. MCP integrations can use this helper when their transport exposes an equivalent challenge metadata field. The SDK does not require merchants to implement AP2 payment settlement separately.
Payment flow
- An unpaid request receives a v2
402challenge with aPAYMENT-REQUIREDheader containing one or more accepted networks, recipient wallets, USDC assets, the amount, and resource URL. - The buyer selects an accepted option, creates an EVM authorization or Solana transaction, and sends it in
PAYMENT-SIGNATURE. - The facilitator verifies and settles the payment.
- USDC settles directly to
payTo; Seedhape never holds merchant funds. - The request continues and the settlement is returned in
PAYMENT-RESPONSE.
The SDK emits the current x402 v2 challenge shape. For migration, it also accepts legacy X-PAYMENT requests and exposes X-PAYMENT-RESPONSE alongside the v2 response header.
Official x402 integrations
The SDK is built on the official @x402/core, @x402/evm, and @x402/svm packages. EIP-3009 and exact-SVM verification and settlement use x402ResourceServer, HTTPFacilitatorClient, and the official chain scheme. Bazaar declarations use @x402/extensions/bazaar and are registered with the resource server.
ERC-7710 remains an opt-in Seedhape facilitator method because the official EVM exact server currently covers EIP-3009 and Permit2, not ERC-7710. When erc7710 is selected, the SDK routes it to the configured facilitator override; it never silently sends an ERC-7710 payload through the EIP-3009 verifier.
Unpaid responses also include this developer notice:
This resource is protected by the x402 payment protocol.
Note to developers: install
@x402/paywallto enable the in-browser wallet connection and payment UI. Programmatic clients should read the payment requirements from the402response headers and JSON body.
Bazaar discovery
Bazaar is an optional discovery layer. Add Bazaar discovery metadata to your x402 route when using an x402 resource server/facilitator that supports the Bazaar extension. Describe the input schema, route, method, and response so agents can find the service without a hardcoded URL. Bazaar does not change settlement or custody.
Sonar discovery
Sonar is Seedhape's service discovery surface for merchants and agents. After deploying your x402 endpoint, list it in Sonar with its public URL, HTTP method, service description, input schema, price, token, network, receiving wallet, and supported payment methods. Sonar helps agents discover and evaluate the service; it does not custody funds or settle payments. The x402 paywall and facilitator still perform verification and settlement.
Sonar listing does not replace Bazaar metadata. A merchant can publish Bazaar discovery metadata for the open x402 ecosystem and list the same endpoint in Sonar for Seedhape discovery.
Production checklist
- Use a self-custodial merchant receiving address.
- Use Base mainnet or Solana mainnet presets in production and their testnet/devnet presets only for testing.
- Configure a production facilitator URL and keep it server-side.
- Keep health, documentation, and discovery metadata routes free.
- Record the receipt ID, resource, payer, amount, network, and transaction hash.
- Treat
payTo, asset, network, and resource as server-controlled values; never accept them from the buyer.
Publishing and full integration guide
Authenticate and verify the package:
npm login
npm whoami
npm run typecheck
npm run build:packages
npm pack --dry-run --workspace=@seedhape/x402-merchant-sdkPublish the public scoped package:
npm publish --workspace=@seedhape/x402-merchant-sdk --access publicFor local tarball testing, versioning, Bazaar metadata, and the complete release checklist, see the repository guide:
License
MIT
