For merchants.

The pieces Steelyard ships on the merchant side — one manifest, five read surfaces, Next.js scaffold, UCP PSP adapters, mandate verification, and x402 paywalls for paid HTTP resources.

Features

00Features
01

defineCommerce

A typed manifest builder that validates at definition time. Catch missing currencies, malformed prices, and duplicate offer IDs before they reach a route handler.

commerce.ts
import { defineCommerce } from "steelyard";

// Validation runs at module load — bad input throws here, not at request time.
export const manifest = defineCommerce({
  identity: {
    name: "Acme Coffee",
    domain: "acme.coffee",
    currencies: ["USD"],
  },
  offers: [
    {
      id: "espresso",
      title: "Single Espresso",
      availability: "in_stock",
      pricing: [{ kind: "one_time", amount: 300, currency: "USD" }],
    },
    {
      id: "cappuccino",
      title: "Cappuccino",
      availability: "in_stock",
      pricing: [{ kind: "one_time", amount: 500, currency: "USD" }],
    },
  ],
});
02

serveCommerce

One call mounts five read surfaces: commerce.json, the plain HTTP API, MCP, ACP, and UCP. Returns a Node Server you can listen() on.

server.ts
import { defineCommerce, serveCommerce } from "steelyard";

const manifest = defineCommerce({
  identity: { name: "Acme Coffee", domain: "acme.coffee", currencies: ["USD"] },
  offers: [
    {
      id: "espresso",
      title: "Single Espresso",
      availability: "in_stock",
      pricing: [{ kind: "one_time", amount: 300, currency: "USD" }],
    },
  ],
});

// Mounts:
//   /.well-known/commerce.json
//   /commerce/products
//   /mcp
//   /acp/feed
//   /.well-known/ucp + /api/catalog/*
serveCommerce(manifest).listen(3000);
03

createCommerceReadHandler

When you already own the HTTP server (Express, Hono, raw Node), createCommerceReadHandler returns a Node RequestListener routed across all five surfaces.

custom-server.ts
import { createServer } from "node:http";
import { defineCommerce, createCommerceReadHandler } from "steelyard";

const manifest = defineCommerce({
  identity: { name: "Acme Coffee", domain: "acme.coffee", currencies: ["USD"] },
  offers: [/* ... */],
});

const commerce = createCommerceReadHandler(manifest);

createServer((req, res) => {
  // Routes commerce.json, /commerce/*, /mcp, /acp/feed, /.well-known/ucp,
  // and /api/catalog/* internally. Anything else returns 404.
  commerce(req, res);
}).listen(3000);
04

steelyard/next adapter

App Router and Pages Router handlers for Next.js. createCommerceRoutes returns one Web Request → Response handler per surface; the CLI writes five route files because UCP discovery and catalog share routes.ucp.

app/mcp/route.ts
// app/mcp/route.ts — generated by `steelyard init`.
import { createCommerceRoutes, resolveManifestModule } from "steelyard/next";
import manifestModule from "@/commerce";

const routes = createCommerceRoutes(
  await resolveManifestModule(manifestModule),
);

export const GET = routes.mcp;
export const POST = routes.mcp;

// Other generated route files (one per surface):
//   app/.well-known/commerce.json/route.ts → routes.wellKnown
//   app/acp/feed/route.ts                  → routes.acpFeed
//   app/.well-known/ucp/route.ts           → routes.ucp
//   app/api/catalog/[...path]/route.ts     → routes.ucp
05

steelyard init

Scaffolds five Steelyard route files into an existing Next.js app. Detects framework, language, package manager; optionally imports your Stripe catalog; installs the public steelyard package. Transactional codegen — every file lands or none does.

shellnext.js scaffold
$ npx steelyard init

 Detected Next.js 15 (App Router · TypeScript · pnpm)
 Stripe API key found in .env.local

? Import your Stripe catalog? (Y/n) Y
 Imported 12 prices across 8 products
 Skipped 2 prices (recurring with trial schema follow-up tracked)

? Manifest file path? (./commerce)
? Install dev inspector at /steelyard? (Y/n) Y
? Tier? Discovery-only (tier A no money moves)

  Writing files...
 app/.well-known/commerce.json/route.ts
 app/mcp/route.ts
 app/acp/feed/route.ts
 app/.well-known/ucp/route.ts
 app/api/catalog/[...path]/route.ts
 app/(steelyard)/steelyard/page.tsx
 commerce.ts (12 offers)
 Installed steelyard

  Next:
    pnpm dev
    open http://localhost:3000/steelyard
06

steelyard enable checkout

Lays the tier-B foundation: requires init to have run; verifies your Stripe key; blocks live-mode keys without --allow-live; probes the account's agentic-payments capability; merges STEELYARD_TIER=b into .env.local without clobbering existing vars. The generated accepting checkout endpoint is a follow-up release.

shelltier B upgrade
$ npx steelyard enable checkout

 This will start accepting agent purchases against your Stripe account.

 Connected to acct_1AbCdEf... (test mode)
 Agentic payments capability: agentic_payments (active)
 Set STEELYARD_TIER=b in .env.local

  Next:
    curl localhost:3000/.well-known/ucp
07

Per-protocol handlers

Only need MCP? Import the specific handler — createMcpHttpHandler, createAcpFeedHandler, createUcpHandler, createHttpApiHandler, createCommerceManifestHandler — from steelyard/protocol/* subpaths and skip the rest.

mcp-only-server.ts
import { createServer } from "node:http";
import { defineCommerce } from "steelyard";
import { createMcpHttpHandler } from "steelyard/protocol/mcp";

const manifest = defineCommerce({
  identity: { name: "Acme Coffee", domain: "acme.coffee", currencies: ["USD"] },
  offers: [/* ... */],
});

// Each create*Handler returns a Node RequestListener you mount wherever
// you want. Skip the surfaces you don't need.
const mcp = createMcpHttpHandler(manifest);

createServer((req, res) => {
  if (req.url?.startsWith("/mcp")) return mcp(req, res);
  res.writeHead(404).end();
}).listen(3000);
08

createCheckoutServer

The checkout server for ACP and UCP. Plug in a PSP, an idempotency store, a session store, and optional UCP mandate verification; UCP payment negotiation is adapter-neutral while ACP remains Stripe SPT-only.

checkout-server.ts
import { createServer } from "node:http";
import { defineCommerce } from "steelyard";
import {
  createCheckoutServer,
  memoryCheckoutSessionStore,
  memoryIdempotencyStore,
} from "steelyard/merchant/checkout";
import { stripePsp } from "steelyard/merchant/psp";

const manifest = defineCommerce({
  identity: { name: "Acme Coffee", domain: "acme.coffee", currencies: ["USD"] },
  offers: [/* ... */],
});

const checkout = createCheckoutServer(manifest, {
  protocols: ["acp", "ucp"],
  psp: stripePsp({ apiKey: process.env.STRIPE_SECRET_KEY!, handlerIds: ["stripe"] }),
  store: memoryCheckoutSessionStore(),
  idempotency: memoryIdempotencyStore(),
  baseUrl: "http://localhost:3001",
});

createServer(checkout.handler).listen(3001);
09

x402 paywall

For paid API routes, x402Paywall returns HTTP 402 challenges and delegates verify and settle to your facilitator client. It does not require a Steelyard manifest and does not replace ACP or UCP checkout.

paid-api.ts
import { createServer } from "node:http";
import { exactUsdc, x402Paywall } from "steelyard";

const paywall = x402Paywall({
  facilitator,
  routes: {
    "GET /paid-weather": exactUsdc({
      amount: "0.001",
      network: "eip155:84532",
      payTo: process.env.X402_PAY_TO!,
      description: "Paid weather API response",
    }),
  },
});

createServer(paywall.handler).listen(3000);
10

Stripe adapter

First-party Stripe adapter, split per side. stripePsp from steelyard/merchant/psp captures funds; stripeSpt from steelyard/stripe/buyer issues Shared Payment Tokens for wallet checkout.

stripe-wiring.ts
import { Wallet } from "steelyard/buyer";
import {
  createCheckoutServer,
  memoryCheckoutSessionStore,
  memoryIdempotencyStore,
} from "steelyard/merchant/checkout";
import { stripePsp } from "steelyard/merchant/psp";
import { stripeSpt } from "steelyard/stripe/buyer";

// Merchant side: capture funds via Stripe.
const checkout = createCheckoutServer(manifest, {
  protocols: ["acp", "ucp"],
  psp: stripePsp({ apiKey: process.env.STRIPE_SECRET_KEY!, handlerIds: ["stripe"] }),
  store: memoryCheckoutSessionStore(),
  idempotency: memoryIdempotencyStore(),
  baseUrl: process.env.PUBLIC_BASE_URL!,
});

// Buyer side: issue Stripe Shared Payment Tokens for supported checkout paths.
const wallet = await Wallet.open({ project: true });
await wallet.addInstrument(stripeSpt({ apiKey: process.env.STRIPE_SECRET_KEY! }));
11

UCP HMS auth on the discovery doc

Advertise that you accept RFC 9421 HTTP Message Signatures on UCP via buildUcpDiscovery's ucp.auth block. createCheckoutServer's ucp config controls verification + merchant-response signing.

ucp-hms.ts
import { buildUcpDiscovery } from "steelyard/protocol/ucp";
import {
  createCheckoutServer,
  memoryCheckoutSessionStore,
  memoryIdempotencyStore,
} from "steelyard/merchant/checkout";
import { stripePsp } from "steelyard/merchant/psp";

const merchantSigningKey = {
  kid: "merchant_2026",
  privateKeyJwk: merchantPrivateJwk,
  algorithm: "ES256" as const,
};

// 1) Discovery: tell buyers your UCP endpoint accepts HMS-signed requests.
const discovery = buildUcpDiscovery(manifest, {
  baseUrl: "https://acme.coffee",
  checkout: true,
  ucp: { auth: { hms: { enabled: true, signingKeys: [merchantPublicJwk] } } },
});

// 2) Checkout: verify incoming signatures + sign outgoing completion responses.
//    Load signing keys from a secure source (KMS, env-encoded JWK, etc.) at boot.
const checkout = createCheckoutServer(manifest, {
  protocols: ["ucp"],
  psp: stripePsp({ apiKey: process.env.STRIPE_SECRET_KEY!, handlerIds: ["stripe"] }),
  store: memoryCheckoutSessionStore(),
  idempotency: memoryIdempotencyStore(),
  baseUrl: "https://acme.coffee",
  ucp: {
    auth: {
      hms: {
        enabled: true,
        signingKeys: [merchantSigningKey],
        activeKid: "merchant_2026",
      },
    },
    responseSigningPolicy: "high-value-only",
  },
});
12

AP2 mandate verification

Enable ucp.ap2 and pass a mandate verifier so the server rejects AP2-locked UCP completions that lack a valid signed mandate. sdJwtKbVerifier wraps the SD-JWT+KB verification flow; mockMandateVerifier is handy for tests.

checkout-with-ap2.ts
import {
  createCheckoutServer,
  memoryCheckoutSessionStore,
  memoryIdempotencyStore,
} from "steelyard/merchant/checkout";
import {
  ap2MerchantAuthorizationSigner,
  memoryNonceStore,
  sdJwtKbVerifier,
} from "steelyard/merchant/mandate";
import { stripePsp } from "steelyard/merchant/psp";

const nonceStore = memoryNonceStore();
const merchantSigningKey = {
  kid: "merchant_2026",
  privateKeyJwk: merchantPrivateJwk,
  algorithm: "ES256" as const,
};

const checkout = createCheckoutServer(manifest, {
  protocols: ["ucp"],
  psp: stripePsp({ apiKey: process.env.STRIPE_SECRET_KEY!, handlerIds: ["stripe"] }),
  store: memoryCheckoutSessionStore(),
  idempotency: memoryIdempotencyStore(),
  baseUrl: "https://acme.coffee",
  ucp: {
    auth: {
      hms: {
        enabled: true,
        signingKeys: [merchantSigningKey],
        activeKid: "merchant_2026",
      },
    },
    ap2: {
      enabled: true,
      nonceStore,
      merchantAuthorizationSigner: ap2MerchantAuthorizationSigner({
        signingKeys: [merchantSigningKey],
        activeKid: "merchant_2026",
      }),
      mandateVerifier: sdJwtKbVerifier({
        trustModel: {
          kind: "digital_payment_credential",
          resolveIssuerKey: async ({ issuer, kid }) => trustedDpcKey({ issuer, kid }),
        },
        expectedAudience: () => "https://acme.coffee/.well-known/ucp",
        nonceStore,
        merchantSigningKeys: [merchantPublicJwk],
      }),
    },
  },
});
13

PSP adapter contract

PspAdapter from steelyard/psp is the public, additive-only adapter interface (name, capabilities, supportsHandler, capture, cancel). Implement it for your rail, then verify with runPspConformance from steelyard/psp/conformance.

my-rail-psp.ts
import type { PspAdapter, PspCaptureArgs } from "steelyard/psp";
import { runPspConformance } from "steelyard/psp/conformance";

export const myRailPsp: PspAdapter = {
  name: "my-rail",
  capabilities: [
    { handlerId: "my-rail", instrumentType: "my-rail-token", idPrefix: "my_" },
  ],

  supportsHandler(handlerId) {
    return handlerId === "my-rail";
  },

  async capture(args: PspCaptureArgs) {
    const charge = await myRail.charge({
      amount: args.amount,
      currency: args.currency,
      token: args.vault_token,
      idempotency_key: args.idempotencyKey,
    });
    return { ok: true, psp_payment_id: charge.id, status: "captured" };
  },

  async cancel({ psp_payment_id, idempotencyKey }) {
    await myRail.cancel({ id: psp_payment_id, idempotency_key: idempotencyKey });
  },
};

// Run the conformance kit locally. Same suite the first-party adapters pass.
const report = await runPspConformance(myRailPsp, { /* fixtures */ });
console.log(report.passed, "/", report.cases.length);
14

Static commerce.json generation

When your catalog rarely changes, generate a static commerce.json at build time and serve it from a CDN. The steelyard manifest CLI accepts a JS module export and writes the canonical JSON with peer URLs filled in.

shellbuild step
# package.json
{
  "scripts": {
    "build:commerce": "steelyard manifest ./dist/catalog.js \\
      --module --export coffeeShopManifest \\
      --peer ucp=https://acme.coffee/.well-known/ucp \\
      --peer mcp=https://acme.coffee/mcp \\
      --peer acp=https://acme.coffee/acp/feed \\
      --protocol-version ucp=2026-04-17 \\
      --protocol-version mcp=0.1 \\
      --protocol-version acp=2026-04-17 \\
      --pretty > public/commerce.json"
  }
}

$ pnpm build:commerce
$ ls -lh public/commerce.json
-rw-r--r--  4.2K  public/commerce.json
15

steelyard doctor

Local runtime health check for the read-side CLI: Node version, built schema files, and schema compilation. It does not probe a running merchant.

shellpreflight
$ npx steelyard doctor

  PASS  node_version
  PASS  commerce_schema
  PASS  http_schemas
  PASS  schema_compile

  All checks passed.

Packages

01Packages
steelyard/next

Next.js adapter. App Router and Pages Router handlers, createCommerceRoutes, and the dev inspector page template that steelyard init writes.

steelyard/cli

The steelyard command. init / enable checkout / manifest / validate / doctor.

steelyard/merchant/checkout

createCheckoutServer, memoryCheckoutSessionStore, memoryIdempotencyStore.

steelyard/merchant/psp

First-party PSP adapters: stripePsp, referencePsp, mockPsp.

steelyard/merchant/mandate

Mandate helpers: sdJwtKbVerifier, mockMandateVerifier, memoryNonceStore, ap2MerchantAuthorizationSigner.

steelyard/x402

x402Paywall, exactUsdc, facilitator client, and memory idempotency store for paid HTTP resource routes.

steelyard/protocol/*

Per-protocol handlers (mcp, acp, ucp, commerce-manifest, http) and buildUcpDiscovery / buildAcpFeed.

steelyard/psp

The public PspAdapter contract + conformance kit you depend on to ship an adapter.

Full reference: steelyard/merchant README ↗