subcent

Merchant Integration Guide

This guide covers how to integrate your business with Subcent to receive payments from AI agents. As a merchant, you register your business, sync your product catalog, receive payments, and handle webhook notifications.

Merchant SDK Installation#

The merchant SDK provides a dedicated client for merchant operations:

npm install @subcent/merchant-sdk

Configuration#

import SubcentMerchant from "@subcent/merchant-sdk";

const merchant = new SubcentMerchant({
  apiKey: "sc_test_your_merchant_key",
  webhookSecret: "whsec_your_webhook_secret",
  network: "testnet",     // "mainnet" or "testnet"
  baseUrl: undefined,     // optional override
});

| Field | Type | Required | Description | |---|---|---|---| | apiKey | string | Yes | Your merchant API key | | webhookSecret | string | Yes | Webhook signing secret (from webhook registration) | | network | string | No | "mainnet" or "testnet" (default: "testnet") | | baseUrl | string | No | Override the API base URL |


Registration Flow#

1. Register Your Business#

Register your merchant account with your wallet address and business details:

TypeScript SDK

2. Verification#

After registration, your merchant account starts in pending_verification status. The verification process confirms wallet ownership. Once verified, your status changes to verified.

| Status | Description | |---|---| | pending_verification | Registered, awaiting wallet verification | | verified | Fully verified, operational | | suspended | Account suspended, payments will be rejected |

3. Choose a Category#

Select the category that best describes your business. This is used by the policy engine when agents have category-based spending restrictions:

groceries, household, electronics, clothing, health, beauty, books, entertainment, api_services, data_services, computing, storage, transportation, food_delivery, subscription


Receiving Payments#

Once registered, AI agents can send payments to your merchant ID. Here is what happens from your perspective:

Payment Flow#

  1. An agent requests a payment targeting your merchant_id
  2. The agent's policy engine evaluates the request
  3. If approved, USDC is transferred to your wallet address on-chain
  4. You receive a payment.completed webhook notification

Monitoring Incoming Payments#

Set up a webhook to monitor incoming payments:

import SubcentMerchant from "@subcent/merchant-sdk";

const merchant = new SubcentMerchant({
  apiKey: "sc_test_your_key",
  webhookSecret: "whsec_your_secret",
});

// Register for payment events
const webhook = await merchant.webhooks.register({
  url: "https://api.freshmart.com/subcent/webhooks",
  events: ["payment.completed", "escrow.funded", "escrow.released"],
});

console.log(webhook.webhook_id);
console.log(webhook.secret);  // new secret for this specific webhook

Listing Webhooks#

const webhooks = await merchant.webhooks.list("vault_id_here");
for (const wh of webhooks) {
  console.log(wh.webhook_id, wh.url, wh.events);
}

Removing a Webhook#

const result = await merchant.webhooks.remove("whk_aa0e8400...");
console.log(result.deleted);  // true

Webhook Verification#

The Merchant SDK includes a built-in webhook verification method that validates the HMAC-SHA256 signature and parses the event:

import express from "express";
import SubcentMerchant from "@subcent/merchant-sdk";

const app = express();
app.use("/webhooks", express.raw({ type: "application/json" }));

const merchant = new SubcentMerchant({
  apiKey: "sc_test_your_key",
  webhookSecret: "whsec_your_secret",
});

app.post("/webhooks/subcent", (req, res) => {
  try {
    const event = merchant.webhooks.verify(
      req.body,                                      // raw body (string or Buffer)
      req.headers["x-subcent-signature"] as string,  // signature header
    );

    // event is typed as WebhookEvent:
    // { event: string, event_id: string, timestamp: string, data: Record<string, unknown> }

    switch (event.event) {
      case "payment.completed":
        console.log(`Payment received: ${event.data.amount} USDC`);
        fulfillOrder(event.data);
        break;

      case "escrow.funded":
        console.log(`Escrow created: ${event.data.escrow_id}`);
        prepareOrder(event.data);
        break;

      case "escrow.released":
        console.log(`Escrow released: ${event.data.escrow_id}`);
        confirmOrderPayment(event.data);
        break;
    }

    res.status(200).json({ received: true });
  } catch (err) {
    // Invalid signature
    console.error("Webhook verification failed:", err.message);
    res.status(401).json({ error: "Invalid signature" });
  }
});

The verifyWebhookSignature function is also exported standalone:

import { verifyWebhookSignature } from "@subcent/merchant-sdk";

const isValid = verifyWebhookSignature(
  "whsec_your_secret",  // webhook secret
  rawBodyString,         // raw request body as string
  signatureHeader,       // X-Subcent-Signature header value
);

This uses crypto.timingSafeEqual internally to prevent timing attacks.


Catalog Management#

Merchants can sync their product catalog with Subcent so AI agents can browse and search products.

Catalog Item Schema#

interface CatalogItem {
  itemId: string;        // Unique identifier
  name: string;          // Product name
  description?: string;  // Product description
  price: string;         // Price in USDC (e.g., "3.99")
  currency: "USDC";      // Currency (always USDC)
  category: string;      // Product category
  subcategory?: string;  // Optional subcategory
  inStock: boolean;      // Availability
  stockQuantity?: number;  // Available quantity
  deliveryDays?: number;   // Estimated delivery time
  deliveryFee?: string;    // Delivery fee in USDC
  rating?: number;         // Average rating (1-5)
  reviewCount?: number;    // Number of reviews
  attributes?: Record<string, unknown>;  // Custom attributes
  images?: string[];       // Product image URLs
}

Syncing Your Catalog#

const result = await merchant.catalog.sync({
  items: [
    {
      itemId: "item_001",
      name: "Organic Bananas",
      description: "Fresh organic bananas, 1 bunch",
      price: "3.99",
      currency: "USDC",
      category: "groceries",
      subcategory: "fruit",
      inStock: true,
      stockQuantity: 150,
      deliveryDays: 1,
      deliveryFee: "0.00",
      rating: 4.8,
      reviewCount: 234,
      images: ["https://freshmart.com/images/bananas.jpg"],
    },
    {
      itemId: "item_002",
      name: "Whole Wheat Bread",
      description: "Freshly baked whole wheat bread",
      price: "4.50",
      currency: "USDC",
      category: "groceries",
      subcategory: "bakery",
      inStock: true,
      stockQuantity: 50,
      deliveryDays: 1,
      deliveryFee: "0.00",
      rating: 4.6,
      reviewCount: 189,
    },
  ],
});

console.log(result.synced);  // 2

Retrieving Your Catalog#

const catalog = await merchant.catalog.get("mrc_your_merchant_id");
console.log(catalog.total_items);
for (const item of catalog.items) {
  console.log(`${item.name}: ${item.price} USDC`);
}

Order Management#

The Merchant SDK provides endpoints to track orders placed by agents:

Listing Orders#

const orders = await merchant.orders.list({ status: "pending", limit: 20 });
for (const order of orders.orders) {
  console.log(order);
}

if (orders.has_more) {
  // Fetch next page
}

Getting Order Details#

const order = await merchant.orders.get("order_123");
console.log(order);

Complete Integration Example#

Here is a full example of a merchant server that handles catalog sync, webhook verification, and order fulfillment:

import express from "express";
import SubcentMerchant from "@subcent/merchant-sdk";

const app = express();
app.use(express.json());
app.use("/webhooks", express.raw({ type: "application/json" }));

const merchant = new SubcentMerchant({
  apiKey: process.env.SUBCENT_MERCHANT_KEY!,
  webhookSecret: process.env.SUBCENT_WEBHOOK_SECRET!,
  network: "mainnet",
});

// Sync catalog on startup
async function syncCatalog() {
  const items = await loadProductsFromDatabase();
  await merchant.catalog.sync({ items });
  console.log("Catalog synced");
}

// Handle incoming webhooks
app.post("/webhooks/subcent", (req, res) => {
  try {
    const event = merchant.webhooks.verify(
      req.body,
      req.headers["x-subcent-signature"] as string,
    );

    switch (event.event) {
      case "payment.completed":
        handlePayment(event.data);
        break;
      case "escrow.funded":
        handleEscrow(event.data);
        break;
    }

    res.status(200).json({ received: true });
  } catch {
    res.status(401).json({ error: "Invalid signature" });
  }
});

async function handlePayment(data: Record<string, unknown>) {
  // Create order in your system
  // Ship the product
  // Update order status
}

async function handleEscrow(data: Record<string, unknown>) {
  // Escrow funded -- prepare the order
  // Once shipped, the vault owner or agent releases the escrow
}

syncCatalog();
app.listen(3000);