npm package: @ppgppgppg/merchant-sdk
Description: x402 Merchant SDK — compliance credential generation and buyer verification for paid APIs
Latest version: 1.0.3

# 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](https://x402.org). 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 header
```

---

## Prerequisites

- Node.js >= 18
- A **merchant account** — obtain the following from your account dashboard:
  - `DASHBOARD_BASE_URL` — the compliance API base URL
  - `AGENTRY_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](https://x402.org) — return HTTP 402 with a `payment-required` header when no payment is present

---

## Install

```bash
npm install @ppgppgppg/merchant-sdk
# or
pnpm add @ppgppgppg/merchant-sdk
```

---

## Complete 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

```typescript
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

```bash
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=3000
```

---

## Request & 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:
```json
{
  "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-XXXXXXXX
```

**Required response header on success:**
```
payment-response: <Base64-encoded SettleResponse JSON>
```

---

## API Reference

### `new AgentryMerchantSDK(config)`

```typescript
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.

```typescript
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>`

```typescript
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.

```typescript
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>`

```typescript
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 `passed` is 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.

```typescript
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>`

```typescript
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:

1. Log in to your merchant dashboard
2. Go to **Tools → Register Tool**
3. 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 |

4. Save — your tool is now discoverable by any agent using the gateway.

> **Tip:** The `description` field 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 into `accepts[0].extra`
- [ ] `payment-required` header set with Base64-encoded PaymentRequired JSON
- [ ] `facilitator.verify()` called before executing any business logic
- [ ] `agentry-payer-id` header read and passed to `verifyPayer()`
- [ ] `verifyPayer()` result checked — return 403 if `passed=false`
- [ ] Business logic runs only after all checks pass
- [ ] `facilitator.settle()` called before sending the 200 response
- [ ] `payment-response` header 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