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#
- An agent requests a payment targeting your
merchant_id - The agent's policy engine evaluates the request
- If approved, USDC is transferred to your wallet address on-chain
- You receive a
payment.completedwebhook 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);