Seller side (x402-server)
x402-server is a framework-agnostic SDK for building x402b-enabled HTTP services. x402-server-express is the Express adapter.
Express
import express from "express"
import { createX402bServerExpress } from "@bosonprotocol/x402-server-express"
import { ethers } from "ethers"
const wallet = new ethers.Wallet(process.env.SELLER_PRIVATE_KEY!)
const app = express()
const x402 = createX402bServerExpress({
network: "polygon",
chainId: 137,
escrow: {
configId: "production-137-0",
sellerId: "12",
productUuid: "ec00f1a5-2c96-4d22-9d80-2a7c12345678",
},
signer: wallet,
facilitator: { url: "https://facilitator.bosonprotocol.io" },
})
app.get("/api/inference", x402.middleware(), async (req, res) => {
// req.x402 is populated: { exchangeId, buyer, paidAt }
const result = await runInference(req.query.prompt)
res.json(result)
})
app.listen(3000)What the middleware does
- Look for
X-PAYMENTheader. - If missing → return 402 with the escrow option built from your config.
- If present → validate (signature, amount, expiry, token).
- POST to the facilitator → wait for on-chain settlement.
- Populate
req.x402with exchange info. - Call your handler.
Configuring the escrow option
Every 402 response advertises one or more accepts payment options. escrow is the one this stack adds.
escrow: {
configId,
sellerId,
productUuid,
// Optional: dynamic price
amount: "1000000", // 1 USDC, in token base units
asset: "0x...", // USDC address on the chain
// Optional: fulfillment options to advertise
fulfillment: { channels: ["inline", "email"] },
}For dynamic pricing, compute these at request time inside your handler.
FullOffer signing hook
When the offer isn't pre-published on-chain (non-listed), the server must sign a FullOffer for the buyer to commit against. The factory takes a signFullOffer hook:
signFullOffer: async (offerDraft) => {
// … your custom logic, then …
return await sdk.signFullOffer(offerDraft)
}Common gotchas
- CORS. The 402 response must include CORS headers if your client is browser-based.
- Idempotency. A retried request with the same
X-PAYMENTmust not double-charge. Cache(paymentHash) → exchangeId. - Facilitator outage. If the facilitator is down, the middleware returns 503. Have a circuit breaker.