Seller SDK
Protect paid resources and verify payment credentials with seller middleware.
Use createSellerMiddleware for Fetch-compatible runtimes and createHonoSellerMiddleware for Hono.
Dashboard-managed resources only need the seller and resource IDs:
import { createOpenPermitClient } from '@openpermit/sdk/client';
import { createSellerMiddleware } from '@openpermit/sdk/seller';
const openpermit = createOpenPermitClient({
baseUrl: 'https://api.openpermit.ai',
apiKey: process.env.OPENPERMIT_API_KEY,
});
const requirePayment = createSellerMiddleware({
client: openpermit,
sellerId: 'seller_...',
resourceId: 'resource_...',
paymentChallengeType: 'x402',
});
export default {
fetch: (request: Request) =>
requirePayment(request, async (_request, context) => {
return Response.json({
ok: true,
intentId: context.intentId,
});
}),
};When credentials are missing or invalid, middleware returns a 402 challenge response. When verification succeeds, it calls your handler with a context containing the verification result, intent ID, payment credential, and optional onchain settlement reference.
Buyer setup actions
For generic-agent checkout, expose a seller-hosted browser checkout-start route and include a buyer setup action on concrete 402 responses. Browser-only agents discover actions.checkoutStart, open it with an item query and quantity, and receive the same concrete setup URL that HTTP-capable agents get from the checkout 402.
Use createSellerCheckoutStartAction in the storefront manifest and createSellerCheckoutStartResponse in the route. The seller still owns catalog resolution, quoting, and checkout-session creation; the SDK standardizes the URL shape and response format.
import {
createOpenPermitBuyerSetupAction,
createSellerCheckoutStartAction,
createSellerCheckoutStartResponse,
parseSellerCheckoutStartRequest,
} from '@openpermit/sdk/seller';
export const manifest = {
actions: {
checkoutStart: createSellerCheckoutStartAction({
url: 'https://storefront.example/agent-payments/start',
}),
},
};
export async function checkoutStart(request: Request) {
const start = parseSellerCheckoutStartRequest(request);
const quote = await quoteBasket(start.items);
const session = await createCheckoutSession(start.items, quote);
const buyerSetup = createOpenPermitBuyerSetupAction({
setupBaseUrl: 'https://openpermit.ai/buyer/setup',
storefrontUrl: 'https://storefront.example',
sellerId: 'seller_...',
resourceId: 'storefront:checkout',
chain: 'eip155:84532',
asset: quote.asset,
amount: quote.amount,
paymentMode: 'metamaskErc7715',
merchantContinuationUrl: session.continuationUrl,
payTo: '0xSellerPayTo...',
tokenAddress: '0xUsdcToken...',
tokenDecimals: 6,
tokenName: 'USDC',
tokenVersion: '2',
});
return createSellerCheckoutStartResponse({
request,
sellerName: 'Example Store',
items: start.items,
idempotencyKey: start.idempotencyKey ?? session.idempotencyKey,
quote,
buyerSetup,
merchantContinuationUrl: session.continuationUrl,
});
}Use createOpenPermitBuyerSetupAction on checkout 402 responses so agents receive a setup URL with the seller/resource, resolved x402 rail metadata, and seller-hosted continuation URL required for autonomous readiness. The seller application should not ask the buyer or agent to pick a chain during checkout; the chain comes from OpenPermit's challenge, which resolves the stored seller resource or provider profile.
import { createOpenPermitBuyerSetupAction, createSellerMiddleware } from '@openpermit/sdk/seller';
const requirePayment = createSellerMiddleware({
client: openpermit,
sellerId: 'seller_...',
resourceId: 'resource_...',
paymentChallengeType: 'x402',
paymentRequired: {
buyerSetup: ({ request, challenge }) => createOpenPermitBuyerSetupAction({
setupBaseUrl: 'https://openpermit.ai/buyer/setup',
storefrontUrl: request.url,
challenge,
paymentMode: 'metamaskErc7715',
merchantContinuationUrl: new URL('/agent-payments/continue/session_123?checkoutSession=checkout_session_123', request.url).toString(),
}),
agentInstructions: ({ buyerSetup }) =>
`Open this OpenPermit setup link. After approval, OpenPermit returns the buyer to this store's continuation page: ${buyerSetup?.nextAction.url}`,
},
});Resolved chain, payTo, tokenAddress, tokenDecimals, tokenName, tokenVersion, ERC-7715 facilitator/redeemer addresses, and merchantContinuationUrl should come from seller resource config, provider defaults, or the x402 challenge. A setup link without those values can onboard the buyer, but it cannot become an agent-ready MetaMask ERC-7715 mandate for that exact seller checkout. Session-backed continuation URLs let generic browser-only agents resume at the merchant page; the seller server calls OpenPermit internally.
Checkout failure display
Seller continuation pages should branch on OpenPermit problem code values, not facilitator or wallet error strings. The SDK exposes getOpenPermitProblemDisplay so integrators can render useful customer copy without hardcoding provider messages:
import { getOpenPermitProblemDisplay } from '@openpermit/sdk/errors';
const response = await fetch('https://api.openpermit.ai/api/v1/agent/commerce/continue', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ continuationToken, items, idempotencyKey }),
});
const body = await response.json();
if (!response.ok) {
const display = getOpenPermitProblemDisplay(body);
return renderCheckoutProblem({
title: display.title,
description: display.description,
details: display.details,
action: display.primaryAction,
});
}For example, a Base Sepolia ERC-7715 payment where the payer account has no USDC returns code: "payment_payer_insufficient_balance", category: "buyer_action_required", actor: "buyer", retryable: true, and a fund_wallet next action with the payer, chain, token, and required amount. Show that action to the buyer; keep raw OpenPermit/provider response data collapsed for diagnostics.
Typed builders
defineSeller and defineSellerResource are typed identity helpers. They return their input unchanged but give your editor full autocomplete and per-field hover docs for the middleware config — no need to import types from @openpermit/types.
import { defineSeller, defineSellerResource } from '@openpermit/sdk/seller';
const seller = defineSeller({
name: 'Example Data API',
domain: 'api.example.com',
});
const quotesResource = defineSellerResource({
resourceId: 'quotes:v1',
method: 'GET',
pathTemplate: '/v1/quotes/{symbol}',
acceptedPaymentChallengeTypes: ['x402'],
pricing: { type: 'fixed', amount: '0.04', asset: 'USDC' },
});Code-managed fixed resources can be declared next to the route. The middleware lazily upserts the seller and resource before the first challenge:
const requirePayment = createSellerMiddleware({
client: openpermit,
seller,
resource: quotesResource,
paymentChallengeType: 'x402',
});Dynamic resources declare pricing bounds and return a runtime quote per request:
const requirePayment = createSellerMiddleware({
client: openpermit,
seller,
resource: defineSellerResource({
resourceId: 'quotes:dynamic',
method: 'GET',
pathTemplate: '/v1/quotes/{symbol}',
acceptedPaymentChallengeTypes: ['x402'],
pricing: { type: 'dynamic', asset: 'USDC', minAmount: '0.01', maxAmount: '0.25', displayAmount: '0.05' },
}),
paymentChallengeType: 'x402',
quote: async (request) => {
const url = new URL(request.url);
return {
amount: url.searchParams.get('tier') === 'realtime' ? '0.12' : '0.03',
quoteId: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 30_000).toISOString(),
metadata: { tier: url.searchParams.get('tier') ?? 'standard' },
};
},
});Use syncSellerResources to apply a deploy manifest:
import { defineSellerResource, syncSellerResources } from '@openpermit/sdk/seller';
await syncSellerResources({
client: openpermit,
sellerId: 'seller_...',
resources: [
defineSellerResource({
resourceId: 'quotes:v1',
method: 'GET',
pathTemplate: '/v1/quotes/{symbol}',
acceptedPaymentChallengeTypes: ['x402'],
pricing: { type: 'fixed', amount: '0.04', asset: 'USDC' },
}),
],
});Hono usage:
import { createHonoSellerMiddleware } from '@openpermit/sdk/seller';
app.use('/paid/*', createHonoSellerMiddleware(requirePaymentConfig));