@ppgppgppg/merchant-sdk
v1.0.3
Published
x402 Merchant SDK — compliance credential generation and buyer verification for paid APIs
Downloads
273
Maintainers
Readme
x402 Merchant SDK
Integrate your API with the x402 payment ecosystem — generate compliance credentials for payment challenges, and verify buyer identity before accepting payment.
When an AI agent calls your API through the x402 gateway, it follows the x402 HTTP payment protocol. This SDK handles the compliance layer on top of x402, so you can focus on building your API.
How it fits in
AI Agent (Cursor / Claude)
│
▼
x402 MCP Gateway ← buyer sends: agentry-payer-id, agentry-intent-vc
│
▼ HTTP (x402 protocol)
Your API ◄── this SDK
│
│ Request 1 — no payment:
│ sdk.createCartMandate() → inject into 402 response header
│
│ Request 2 — payment present:
│ x402 facilitator verify()
│ sdk.verifyPayer() → KYC + AP2 compliance check
│ business logic
│ x402 facilitator settle() → on-chain USDC transfer
▼
Return 200 + payment-response headerPrerequisites
- Node.js >= 18
- A merchant account — obtain the following from your account dashboard:
DASHBOARD_BASE_URL— the compliance API base URLAGENTRY_API_KEY— your merchant API key (agt_...)AGENTRY_MERCHANT_ID— your merchant ID (AGT-...)
- An EVM wallet address to receive USDC payments
- Your API must implement the x402 protocol — return HTTP 402 with a
payment-requiredheader when no payment is present
Install
npm install @ppgppgppg/merchant-sdk
# or
pnpm add @ppgppgppg/merchant-sdkComplete Working Example (Express + x402)
Below is a production-ready Express endpoint that:
- Charges 0.10 USDC per request
- Generates a compliance credential (CartMandate) on the first call
- Verifies buyer identity (KYC + AP2) before accepting payment
- Settles on-chain via the x402 Facilitator
import express from "express";
import { AgentryMerchantSDK } from "@ppgppgppg/merchant-sdk";
import {
encodePaymentRequiredHeader,
encodePaymentResponseHeader,
decodePaymentSignatureHeader,
} from "@x402/core/http";
import { HTTPFacilitatorClient } from "@x402/core/server";
const app = express();
app.use(express.json());
// ── 1. Initialize the SDK ──────────────────────────────────────────────
const sdk = new AgentryMerchantSDK({
dashboardBaseUrl: process.env.DASHBOARD_BASE_URL!,
apiKey: process.env.AGENTRY_API_KEY!,
merchantId: process.env.AGENTRY_MERCHANT_ID!,
merchantAddress: process.env.MERCHANT_WALLET_ADDRESS!,
payTo: process.env.MERCHANT_WALLET_ADDRESS!,
});
// ── 2. Initialize the x402 Facilitator (on-chain settlement) ─────────
const facilitator = new HTTPFacilitatorClient(
process.env.FACILITATOR_URL ?? "https://x402.org/facilitator"
);
// ── 3. Define payment requirements ───────────────────────────────────
const PRICE_ATOMIC = "100000"; // 0.10 USDC (6 decimals)
const paymentRequirements = {
scheme: "exact",
network: "eip155:84532", // Base Sepolia testnet
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
amount: PRICE_ATOMIC,
payTo: process.env.MERCHANT_WALLET_ADDRESS!,
maxTimeoutSeconds: 300,
extra: { name: "USDC", version: "2" },
};
// ── 4. Your paid endpoint ─────────────────────────────────────────────
app.get("/api/data", async (req, res) => {
const signatureHeader = req.headers["payment-signature"] as string | undefined;
// ── Request 1: No payment — return 402 challenge ──────────────────
if (!signatureHeader) {
const resourceUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const paymentRequired = {
x402Version: 2,
error: "Payment required to access this resource",
resource: { url: resourceUrl, mimeType: "application/json" },
accepts: [{ ...paymentRequirements }],
};
// Generate CartMandate — fail-close: if this fails, return 503
let cartMandate;
try {
cartMandate = await sdk.createCartMandate({ price: "0.10", currency: "USDC" });
} catch (err) {
res.status(503).json({ error: "CART_MANDATE_FAILED", message: "Try again later." });
return;
}
// Inject CartMandate into the payment challenge
paymentRequired.accepts[0].extra = {
...paymentRequired.accepts[0].extra,
cartMandate,
};
// Optional: bidirectional compliance check (recommended)
// If the gateway sent the buyer's intent VC, verify compatibility now
// so incompatible buyers are rejected before the payment attempt
const intentVcHeader = req.headers["agentry-intent-vc"] as string | undefined;
if (intentVcHeader) {
let intentVcJson: Record<string, unknown>;
try {
intentVcJson = JSON.parse(Buffer.from(intentVcHeader, "base64").toString("utf-8"));
} catch {
res.status(400).json({ error: "INVALID_INTENT_VC_HEADER" });
return;
}
const compatResult = await sdk.verifyBuyerIntent(intentVcJson, cartMandate.vcJson);
if (!compatResult.passed) {
res.status(403).json({
error: "INTENT_INCOMPATIBLE",
code: compatResult.errorCode,
message: compatResult.reason,
});
return;
}
}
res.setHeader("payment-required", encodePaymentRequiredHeader(paymentRequired as any));
res.status(402).json({ error: "Payment required" });
return;
}
// ── Request 2: Payment present — verify, check compliance, settle ─
let paymentPayload;
try {
paymentPayload = decodePaymentSignatureHeader(signatureHeader);
} catch {
res.status(400).json({ error: "INVALID_PAYMENT_SIGNATURE" });
return;
}
// Step A: Verify payment signature on-chain via x402 Facilitator
let verifyResult;
try {
verifyResult = await facilitator.verify(paymentPayload, paymentRequirements as any);
} catch (err) {
res.status(502).json({ error: "FACILITATOR_UNAVAILABLE" });
return;
}
if (!verifyResult.isValid) {
res.status(402).json({ error: "PAYMENT_VERIFICATION_FAILED", reason: (verifyResult as any).invalidReason });
return;
}
// Step B: Verify buyer identity — KYC + AP2 compliance
const payerFacilitateId = req.headers["agentry-payer-id"] as string | undefined;
if (!payerFacilitateId) {
res.status(403).json({ error: "MISSING_PAYER_ID" });
return;
}
try {
const verification = await sdk.verifyPayer(payerFacilitateId);
if (!verification.passed) {
res.status(403).json({ error: "COMPLIANCE_CHECK_FAILED", reason: verification.reason });
return;
}
} catch {
res.status(503).json({ error: "COMPLIANCE_CHECK_ERROR" });
return;
}
// Step C: Execute your business logic here
const responseData = { message: "Here is your data", timestamp: new Date().toISOString() };
// Step D: Settle on-chain and return response
const settleResult = await facilitator.settle(paymentPayload, paymentRequirements as any);
res.setHeader("payment-response", encodePaymentResponseHeader(settleResult as any));
res.json(responseData);
});
app.listen(process.env.PORT ?? 3000, () => {
console.log("Server running on port", process.env.PORT ?? 3000);
});Environment Variables
DASHBOARD_BASE_URL=https://your-dashboard-url # provided by the platform
AGENTRY_API_KEY=agt_your_api_key # from your merchant account
AGENTRY_MERCHANT_ID=AGT-XXXXX # from your merchant account
MERCHANT_WALLET_ADDRESS=0xYourEVMWalletAddress # EVM address to receive USDC
FACILITATOR_URL=https://x402.org/facilitator # x402 on-chain settlement
PORT=3000Request & Response Flow
First Request (no payment-signature header)
Your API receives a regular HTTP request. Since there is no payment, return HTTP 402 with a payment-required header.
Required response header:
payment-required: <Base64-encoded PaymentRequired JSON>The PaymentRequired JSON structure:
{
"x402Version": 2,
"error": "Payment required",
"resource": { "url": "https://your-api.com/endpoint", "mimeType": "application/json" },
"accepts": [{
"scheme": "exact",
"network": "eip155:84532",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "100000",
"payTo": "0xYourWalletAddress",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USDC",
"version": "2",
"cartMandate": { /* generated by sdk.createCartMandate() */ }
}
}]
}Optional incoming header (bidirectional compliance):
agentry-intent-vc: <Base64-encoded IntentVC JSON>If present, call sdk.verifyBuyerIntent() before returning the 402. Returns 403 if buyer's budget is insufficient.
Second Request (with payment-signature header)
Incoming headers:
payment-signature: <Base64-encoded PaymentPayload JSON>
agentry-payer-id: FACILITATE-XXXXXXXXRequired response header on success:
payment-response: <Base64-encoded SettleResponse JSON>API Reference
new AgentryMerchantSDK(config)
interface AgentryMerchantSDKConfig {
dashboardBaseUrl: string; // Compliance API base URL (from your account)
apiKey: string; // Your merchant API key (agt_...)
merchantId: string; // Your merchant ID (AGT-...)
merchantAddress: string; // Your EVM wallet address
payTo: string; // EVM address to receive USDC (usually same as merchantAddress)
timeoutMs?: number; // Per-request timeout in ms. Default: 8000
maxRetries?: number; // Max retry attempts. Default: 5
}sdk.createCartMandate(params)
Call when: Handling the first request (no payment-signature).
Generates a signed compliance credential (W3C Verifiable Credential) for this transaction. Must be injected into accepts[0].extra.cartMandate in your payment-required header.
const cartMandate = await sdk.createCartMandate({ price: "0.10", currency: "USDC" });| Parameter | Type | Description |
|-----------|------|-------------|
| price | string | Human-readable amount, e.g. "0.10" |
| currency | string | Currency symbol, e.g. "USDC" |
Returns: Promise<CartMandate>
interface CartMandate {
mandateId: string;
merchant_id: string;
total_amount: string;
currency: string;
vcJson: Record<string, unknown>; // W3C VC — inject into 402 extra
expiresAt: string; // ISO 8601
}Fail-close rule: If this throws, return HTTP 503. Never skip the CartMandate. The gateway will reject payments without it.
sdk.verifyPayer(payerFacilitateId)
Call when: Handling the second request (after facilitator.verify() succeeds).
Runs KYC check and AP2 intent verification for the buyer in parallel.
const payerFacilitateId = req.headers["agentry-payer-id"] as string;
const result = await sdk.verifyPayer(payerFacilitateId);
if (!result.passed) {
res.status(403).json({ error: "COMPLIANCE_CHECK_FAILED", reason: result.reason });
return;
}| Parameter | Type | Description |
|-----------|------|-------------|
| payerFacilitateId | string | Buyer's facilitateId (value of the agentry-payer-id request header) |
Returns: Promise<PayerVerificationResult>
interface PayerVerificationResult {
passed: boolean;
kycResult?: { kycCompleted: boolean; kycStatus: "approved" | "pending" | "rejected" | null };
ap2Result?: { found: boolean };
reason?: string; // human-readable failure reason when passed=false
}Fail-close rule: If
passedis false, reject (403). If the call throws, reject (503). Never serve a response when compliance fails.
sdk.verifyBuyerIntent(intentVcJson, cartMandateVcJson)
Call when: Handling the first request, if the agentry-intent-vc header is present.
Performs a bidirectional compatibility check — verifies that the buyer's spending mandate is compatible with your CartMandate (currency match, per-transaction limit, total budget). Allows you to reject incompatible buyers early, before any payment attempt.
const compatResult = await sdk.verifyBuyerIntent(intentVcJson, cartMandate.vcJson);
if (!compatResult.passed) {
res.status(403).json({
error: "INTENT_INCOMPATIBLE",
code: compatResult.errorCode,
message: compatResult.reason,
});
return;
}| Parameter | Type | Description |
|-----------|------|-------------|
| intentVcJson | Record<string, unknown> | Buyer's intent VC (decoded from agentry-intent-vc header) |
| cartMandateVcJson | Record<string, unknown> | Your CartMandate VC (cartMandate.vcJson from createCartMandate()) |
Returns: Promise<IntentCompatibilityResult>
interface IntentCompatibilityResult {
passed: boolean;
errorCode?: "INTENT_VC_INVALID" | "BUDGET_EXCEEDED" | "PER_TX_LIMIT_EXCEEDED"
| "CURRENCY_MISMATCH" | "COMPATIBILITY_FAILED" | "VERIFY_BATCH_ERROR";
reason?: string;
compatibility?: {
compatible: boolean;
currencyMatch: boolean;
withinPerTxLimit: boolean;
withinTotalBudget: boolean;
cartAmount: string;
remainingBudget: string | null;
};
}This method is optional but recommended. If the header is absent (older gateway versions), skip the call and proceed normally.
Error Reference
| HTTP Status | Error Code | When it occurs |
|-------------|-----------|----------------|
| 400 | INVALID_PAYMENT_SIGNATURE | payment-signature header is malformed |
| 400 | INVALID_INTENT_VC_HEADER | agentry-intent-vc header is not valid Base64 JSON |
| 402 | PAYMENT_VERIFICATION_FAILED | x402 Facilitator rejected the payment signature |
| 403 | MISSING_PAYER_ID | agentry-payer-id header is missing on second request |
| 403 | COMPLIANCE_CHECK_FAILED | Buyer failed KYC or AP2 check |
| 403 | INTENT_INCOMPATIBLE | Buyer's budget is insufficient for this transaction |
| 502 | FACILITATOR_UNAVAILABLE | Could not reach the x402 Facilitator |
| 503 | CART_MANDATE_FAILED | Could not generate CartMandate (compliance service unavailable) |
| 503 | COMPLIANCE_CHECK_ERROR | Compliance service threw an unexpected error |
Register Your API
Once your API is live and passing payments, register it so AI agents can discover and call it:
- Log in to your merchant dashboard
- Go to Tools → Register Tool
- Fill in the following fields:
| Field | Example | Description |
|-------|---------|-------------|
| Tool Name | weather_get_current | Unique identifier used as the MCP tool name by the AI agent |
| Description | "Get current weather for a city" | The AI reads this to decide when to call your tool |
| Input Schema | { "type": "object", "properties": { "city": { "type": "string" } } } | JSON Schema of the parameters your endpoint accepts |
| Endpoint URL | https://your-api.com/v1/weather | Full URL of your paid endpoint |
| HTTP Method | GET or POST | HTTP method |
| Price | 0.10 USDC | Amount charged per call |
- Save — your tool is now discoverable by any agent using the gateway.
Tip: The
descriptionfield directly influences when the AI decides to call your tool. Make it specific and action-oriented: "Get real-time stock price for a given ticker symbol" is better than "Stock data".
Checklist Before Going Live
- [ ]
createCartMandate()called on first request, CartMandate injected intoaccepts[0].extra - [ ]
payment-requiredheader set with Base64-encoded PaymentRequired JSON - [ ]
facilitator.verify()called before executing any business logic - [ ]
agentry-payer-idheader read and passed toverifyPayer() - [ ]
verifyPayer()result checked — return 403 ifpassed=false - [ ] Business logic runs only after all checks pass
- [ ]
facilitator.settle()called before sending the 200 response - [ ]
payment-responseheader set on 200 response - [ ] All errors follow fail-close: 503 if compliance service is down, never serve without verification
- [ ] Tool registered on the merchant dashboard
License
MIT
