npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ppgppgppg/merchant-sdk

v1.0.7

Published

x402 Merchant SDK — compliance credential generation and buyer verification for paid APIs

Readme

x402 Merchant SDK

Centralized project documentation lives in ../docs. This README remains the package-level SDK reference for npm users.

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: x402-payer-id, x402-intent-vc, x402-payer-kyc-vc
    │
    ▼ HTTP (x402 protocol)
Your API  ◄── this SDK
    │
    │  Request 1 — no payment:
    │    sdk.createPaymentChallengeExtras() → inject into 402 response header
    │
    │  Request 2 — payment present:
    │    x402 facilitator verify()
    │    sdk.verifyPayer()        → local buyer KYC VC check
    │    business logic
    │    x402 facilitator settle() → on-chain USDC transfer
    ▼
Return 200 + payment-response header

Prerequisites

  • Node.js >= 18
  • A merchant account — apply at toani Dashboard to obtain the following credentials:
    • DASHBOARD_BASE_URL — the compliance API base URL (https://test-agentry-dashboard.zk.me/)
    • FACILITATE_MERCHANT_API_KEY — your merchant API key (facilitate_...)
    • FACILITATE_ID — your merchant ID (FACILITATE-...)
  • An EVM wallet address to receive USDC payments
  • Your API must implement the x402 protocol — return HTTP 402 with a payment-required header when no payment is present

Apply for a Merchant Account

  1. Visit toani Dashboard and register an account
  2. After logging in, go to Settings → API Keys page
  3. Click Generate API Key to create a new Merchant API Key (format: facilitate_...)
  4. View your Merchant ID (format: FACILITATE-...) on the account information page
  5. Configure your EVM wallet address as the payment recipient

Install

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 CartMandate and merchant KYC VC on the first call
  • Tells the gateway which buyer KYC fields must be disclosed
  • Verifies buyer KYC VC before accepting payment
  • Settles on-chain via the x402 Facilitator
import express from "express";
import { MerchantSDK } 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 MerchantSDK({
  dashboardBaseUrl: process.env.DASHBOARD_BASE_URL!,
  apiKey: process.env.FACILITATE_MERCHANT_API_KEY!,
  merchantId: process.env.FACILITATE_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 + merchant KYC VC + buyer disclosure requirement.
    // Fail-close: if these fail, return 503.
    let paymentChallengeExtras;
    try {
      paymentChallengeExtras = await sdk.createPaymentChallengeExtras({
        price: "0.10",
        currency: "USDC",
      });
    } catch (err) {
      res
        .status(503)
        .json({ error: "PAYMENT_CHALLENGE_EXTRAS_FAILED", message: "Try again later." });
      return;
    }

    // Inject CartMandate, merchant KYC VC, and payer disclosure requirement into the payment challenge.
    paymentRequired.accepts[0].extra = {
      ...paymentRequired.accepts[0].extra,
      ...paymentChallengeExtras,
    };

    // 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["x402-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,
        paymentChallengeExtras.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 — local KYC VC compliance
  const payerFacilitateId = req.headers["x402-payer-id"] as string | undefined;
  if (!payerFacilitateId) {
    res.status(403).json({ error: "MISSING_PAYER_ID" });
    return;
  }

  const payerKycVcHeader = req.headers["x402-payer-kyc-vc"] as
    | string
    | undefined;
  if (!payerKycVcHeader) {
    res.status(403).json({ error: "MISSING_PAYER_KYC_VC" });
    return;
  }

  let payerKycVc;
  try {
    payerKycVc = JSON.parse(
      Buffer.from(payerKycVcHeader, "base64").toString("utf-8"),
    );
  } catch {
    res.status(400).json({ error: "INVALID_PAYER_KYC_VC_HEADER" });
    return;
  }

  try {
    const payerKycRequirement = await sdk.getPayerKycRequirement();
    const verification = await sdk.verifyPayer(payerFacilitateId, {
      payerKycVc,
      fields: payerKycRequirement.fields,
    });
    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://test-agentry-dashboard.zk.me/  # toani Dashboard URL
FACILITATE_MERCHANT_API_KEY=agt_your_api_key               # Get from Dashboard (Settings → API Keys)
FACILITATE_ID=AGT-XXXXX                                    # Get from Dashboard (Account page)
MERCHANT_WALLET_ADDRESS=0xYourEVMWalletAddress             # Your EVM wallet address (receives 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:

{
  "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() */
        },
        "merchantKycVc": {
          /* generated by sdk.getMerchantKycVc() */
        },
        "payerKycRequirement": { "fields": ["kycStatus"] }
      }
    }
  ]
}

Optional incoming header (bidirectional compliance):

x402-intent-vc: <Base64-encoded IntentVC JSON>

If present, call sdk.verifyBuyerIntent() before returning the 402. Returns 403 if buyer's budget is insufficient.

Required outgoing extra fields for KYC VC flow:

The recommended path is to inject the object returned by sdk.createPaymentChallengeExtras({ price, currency }) directly into accepts[0].extra.

| Field | Description | | --------------------- | ------------------------------------------------------------------------------------------------------- | | cartMandate | AP2 CartMandate from sdk.createCartMandate() | | merchantKycVc | Merchant KYC VC from sdk.getMerchantKycVc(["kycStatus"]); the gateway verifies this before paying | | payerKycRequirement | Disclosure requirement from sdk.getPayerKycRequirement(); the gateway uses this to fetch buyer KYC VC |

Second Request (with payment-signature header)

Incoming headers:

payment-signature: <Base64-encoded PaymentPayload JSON>
x402-payer-id: FACILITATE-XXXXXXXX
x402-payer-kyc-vc: <Base64-encoded buyer KYC VC JSON>

Required response header on success:

payment-response: <Base64-encoded SettleResponse JSON>

API Reference

new MerchantSDK(config)

interface MerchantSDKConfig {
  dashboardBaseUrl: string; // Compliance API base URL (from your account)
  apiKey: string; // Your merchant API key (facilitate_...)
  merchantId: string; // Your merchant ID (FACILITATE-...)
  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
}

The SDK does not load .env files by itself. If your app reads credentials from .env, load them in your application entrypoint, for example with import "dotenv/config"; or dotenv.config(), before constructing MerchantSDK.


sdk.createPaymentChallengeExtras(params)

Call when: Handling the first request (no payment-signature).

Recommended high-level helper. It creates the AP2 CartMandate, fetches the merchant's own KYC VC with ["kycStatus"], and loads the merchant-configured buyer disclosure requirement from Dashboard.

const paymentChallengeExtras = await sdk.createPaymentChallengeExtras({
  price: "0.10",
  currency: "USDC",
});

paymentRequired.accepts[0].extra = {
  ...paymentRequired.accepts[0].extra,
  ...paymentChallengeExtras,
};

Returns: Promise<PaymentChallengeExtras>

interface PaymentChallengeExtras {
  cartMandate: CartMandate;
  merchantKycVc: VcVerificationInput;
  payerKycRequirement: { fields: string[] };
}

Fail-close rule: If this throws, return HTTP 503. Do not return a partial 402 challenge.


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.getPayerKycRequirement()

Call when: Handling the first request (no payment-signature).

Fetches the merchant-configured buyer KYC disclosure fields from Dashboard. If Dashboard returns no fields, the SDK defaults to ["kycStatus"]. Dashboard request failures, auth failures, or non-success business codes throw.

const payerKycRequirement = await sdk.getPayerKycRequirement();

paymentRequired.accepts[0].extra = {
  ...paymentRequired.accepts[0].extra,
  payerKycRequirement,
};

Returns: Promise<{ fields: string[] }>


sdk.getMerchantKycVc(fields?)

Call when: Handling the first request (no payment-signature).

Fetches the merchant account's own KYC VC from Dashboard. Inject it into accepts[0].extra.merchantKycVc; the gateway verifies this before signing payment.

const merchantKycVc = await sdk.getMerchantKycVc(["kycStatus"]);

| Parameter | Type | Description | | --------- | ---------- | ------------------------------------------------------ | | fields | string[] | Disclosure fields to include. Default: ["kycStatus"] |

Returns: Promise<VcVerificationInput>


sdk.verifyPayerKycVc(payerKycVc, fields?)

Call when: You need to verify a buyer KYC VC directly.

This validates the VC signature, time window, disclosure hashes, and required fields. The kycStatus claim must equal "verified".

const result = await sdk.verifyPayerKycVc(payerKycVc, ["kycStatus"]);
if (!result.passed) {
  res.status(403).json({ error: "KYC_VC_FAILED", reason: result.reason });
  return;
}

| Parameter | Type | Description | | ------------ | --------------------- | ---------------------------------------------------- | | payerKycVc | VcVerificationInput | Buyer KYC VC decoded from x402-payer-kyc-vc | | fields | string[] | Required disclosure fields. Default: ["kycStatus"] |

Returns: Promise<VcVerificationResult>


sdk.verifyPayer(payerFacilitateId, options)

Call when: Handling the second request (after facilitator.verify() succeeds).

Runs local buyer KYC VC verification. Missing or invalid payerKycVc fails closed.

For stronger AP2 protection, pass both intentVcJson and cartMandateVcJson when they are available. verifyPayer() will then also verify that the buyer's IntentMandate is compatible with the CartMandate for this payment. Existing integrations that only pass payerKycVc and fields perform only the local KYC VC check.

const payerFacilitateId = req.headers["x402-payer-id"] as string;
const payerKycVc = JSON.parse(
  Buffer.from(req.headers["x402-payer-kyc-vc"] as string, "base64").toString(
    "utf-8",
  ),
);
const payerKycRequirement = await sdk.getPayerKycRequirement();

const result = await sdk.verifyPayer(payerFacilitateId, {
  payerKycVc,
  fields: payerKycRequirement.fields,
  // Optional but recommended when your handler has both values:
  intentVcJson,
  cartMandateVcJson,
});

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 x402-payer-id request header) | | options.payerKycVc | VcVerificationInput | Buyer KYC VC decoded from the x402-payer-kyc-vc request header | | options.fields | string[] | Required KYC disclosure fields. Default: ["kycStatus"] | | options.intentVcJson | Record<string, unknown> | Optional buyer IntentMandate VC for AP2/cart compatibility verification | | options.cartMandateVcJson | Record<string, unknown> | Optional merchant CartMandate VC for AP2/cart compatibility verification |

Returns: Promise<PayerVerificationResult>

interface PayerVerificationResult {
  passed: boolean;
  kycResult?: VcVerificationResult;
  intentCompatibilityResult?: IntentCompatibilityResult;
  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 x402-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 x402-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 remains useful for early rejection before returning a 402. For second-request verification, you can pass the same intentVcJson and cartMandateVcJson to verifyPayer() so KYC and AP2/cart compatibility are reported in one result. verifyPayer() does not perform a separate AP2 intent existence check from payerFacilitateId.


sdk.verifyVcCredential(input, options?)

Call when: You receive a VC JSON object directly and want to verify it locally inside your service (without dashboard verify APIs).

This method follows the full partner verification rules:

  • Fetch public key from DID document (kid -> verificationMethod.id)
  • Verify Ed25519 JWT signature (header.payload, signature)
  • Validate issuedAt/expiresAt and JWT nbf/exp
  • Validate each disclosure hash exists in payload _sd, then decode and check field-name consistency
const vcInput = {
  credentialId: "urn:uuid:3fd5ed88-1e27-4ab6-af25-9c3e1133a098",
  issuedAt: "2026-04-15T08:41:14Z",
  expiresAt: "2027-04-15T08:41:14Z",
  jwt: "header.payload.signature",
  disclosures: {
    kycStatus:
      "WyI5OWZZamdVbDJEMkFaYmp6SnFDVXlRIiwia3ljU3RhdHVzIiwidmVyaWZpZWQiXQ",
  },
};

const result = await sdk.verifyVcCredential(vcInput, {
  didDocumentUrl: "https://test-toani-kyc-credential.zk.me/.well-known/did.json",
  didCacheTtlMs: 300000,
  clockSkewSec: 5,
});

if (!result.passed) {
  console.error(result.errorCode, result.reason);
  return;
}

console.log("verified claims:", result.verifiedClaims);

| Parameter | Type | Description | | ------------------------ | --------------------- | -------------------------------------------------------------------------- | | input | VcVerificationInput | VC payload (credentialId, issuedAt, expiresAt, jwt, disclosures) | | options.didDocumentUrl | string | DID document URL. Default: https://test-toani-kyc-credential.zk.me/.well-known/did.json | | options.didCacheTtlMs | number | DID cache TTL in milliseconds. Default: 300000 | | options.clockSkewSec | number | Allowed clock skew in seconds. Default: 0 |

Returns: Promise<VcVerificationResult>

interface VcVerificationResult {
  passed: boolean;
  errorCode?:
    | "INVALID_INPUT"
    | "INVALID_JWT_FORMAT"
    | "INVALID_JWT_HEADER"
    | "UNSUPPORTED_ALGORITHM"
    | "KID_NOT_FOUND"
    | "PUBLIC_KEY_NOT_FOUND"
    | "INVALID_SIGNATURE"
    | "TIME_WINDOW_INVALID"
    | "JWT_TIME_INVALID"
    | "INVALID_DISCLOSURE"
    | "DISCLOSURE_HASH_MISMATCH"
    | "DISCLOSURE_KEY_MISMATCH"
    | "INVALID_PAYLOAD";
  reason?: string;
  kid?: string;
  verifiedClaims: Record<string, unknown>;
  disclosureCount: number;
  matchedDisclosureCount: number;
}

Production notes:

  • Use the latest key from DID document, do not hardcode test keys.
  • Keep fail logs (errorCode + reason) for troubleshooting and auditing.
  • If verification fails, reject request (fail-close), do not continue business flow.

Error Reference

| HTTP Status | Error Code | When it occurs | | ----------- | ----------------------------- | --------------------------------------------------------------- | | 400 | INVALID_PAYMENT_SIGNATURE | payment-signature header is malformed | | 400 | INVALID_INTENT_VC_HEADER | x402-intent-vc header is not valid Base64 JSON | | 400 | INVALID_PAYER_KYC_VC_HEADER | x402-payer-kyc-vc header is not valid Base64 JSON | | 402 | PAYMENT_VERIFICATION_FAILED | x402 Facilitator rejected the payment signature | | 403 | MISSING_PAYER_ID | x402-payer-id header is missing on second request | | 403 | MISSING_PAYER_KYC_VC | x402-payer-kyc-vc header is missing on second request | | 403 | COMPLIANCE_CHECK_FAILED | Buyer failed KYC VC 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, register it on the Dashboard so AI agents can discover and call it:

  1. Log in to toani 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 |

  1. Save — your tool is now discoverable by any agent using the toani 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

  • [ ] createPaymentChallengeExtras() called on first request, full result injected into accepts[0].extra
  • [ ] payment-required header set with Base64-encoded PaymentRequired JSON
  • [ ] facilitator.verify() called before executing any business logic
  • [ ] x402-payer-id and x402-payer-kyc-vc headers read before 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