Seller/Server Guide
Scaffold, configure, and deploy a Bun and Hono server that accepts x402 payments.
Create, customize, verify, and deploy an x402 v2 server with the official Naven CLI template. The generated project is a standalone Bun and Hono application with a protected example endpoint, validation, tests, and Docker support.
Prerequisites
- Bun 1.2 or later
- An EVM address that will receive payments
Create a server
bunx naven-cli@latest create server my-server
cd my-serverThe command copies the template into my-server, creates .env, and installs dependencies. It will not overwrite a non-empty directory.
Pass --no-install if you want to inspect the template before installing:
bunx naven-cli@latest create server my-server --no-install
cd my-server
bun installIf you use Naven regularly, install the CLI globally:
bun add --global naven-cli@latest
naven --version
naven create server my-serverSee the Naven CLI reference for all commands, options, generated scripts, and troubleshooting.
Understand the generated template
The CLI creates an independent application. You do not need naven-cli installed to run or deploy it after generation.
my-server/
├── .env
├── .env.example
├── Dockerfile
├── README.md
├── package.json
├── scripts/
│ └── test-challenge.ts
└── src/
├── app.ts
├── config.ts
└── index.ts| File | What to change |
|---|---|
.env | Payment recipient, public origin, port, facilitator, and CORS origin. |
src/app.ts | Paid route definitions, resource metadata, and service handlers. |
src/config.ts | Network, token, price, timeout, and environment validation. |
src/index.ts | Bun server startup behavior. |
scripts/test-challenge.ts | Challenge assertions after renaming or adding paid routes. |
Dockerfile | Container runtime details when your platform needs different defaults. |
Configure
The command creates .env from .env.example. Set the EVM address that should receive payments. This is a public recipient address, not a private key:
X402_PAY_TO=0xYourEvmAddressFor local development, the remaining defaults are ready to use:
PUBLIC_URL=http://localhost:4021
PORT=4021
FACILITATOR_URL=https://facilitator.naven.network
CORS_ORIGIN=*Set PUBLIC_URL to the public HTTPS origin when you deploy the server. It must be an origin without a path, query, or fragment. For example, use https://api.example.com, not https://api.example.com/v1.
Run
bun run devThe generated project includes:
GET /health, a free health checkGET /weather, an x402-protected example- x402 v2 middleware for Bun and Hono
- Robinhood Chain USDG payment configuration
- environment and wallet-address validation
- browser CORS and payment response headers
- a Dockerfile and TypeScript checks
The default payment terms are:
| Setting | Value |
|---|---|
| Network | Robinhood Chain (eip155:4663) |
| Asset | USDG (0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168) |
| Price | 100 atomic units, or 0.0001 USDG |
| Payment timeout | 300 seconds |
Verify the challenge
Run the included smoke test. It verifies the server returns a valid PAYMENT-REQUIRED challenge without spending funds:
bun run test:challengeYou can also inspect the response directly:
curl -i http://localhost:4021/weatherExpected responses:
GET /healthreturns200 OK.GET /weatherwithout a payment returns402 Payment Requiredand aPAYMENT-REQUIREDheader.
The challenge test does not submit or settle an onchain payment.
Add your service
Open src/app.ts and replace the example handler with your service logic. Do expensive work only inside the protected handler so unpaid requests cannot trigger it:
app.get("/weather", async c => {
const report = await loadYourPaidData();
return c.json({ report });
});If you rename /weather or change its HTTP method, update both the route key in paymentMiddleware and the Hono handler. They must match exactly:
// Payment configuration
"GET /your-resource": {
// accepts, resource metadata, and price
}
// Protected handler
app.get("/your-resource", async c => {
return c.json({ result: await loadYourPaidData() });
});Also update the resource, description, mimeType, and serviceName metadata shown to buyers:
resource: new URL("/your-resource", config.publicUrl).toString(),
description: "Describe what the buyer receives",
mimeType: "application/json",
serviceName: "Your service name",If you change the example route, update scripts/test-challenge.ts so resourceUrl points to the same path.
Set the price
Change priceAtomic in src/config.ts to set a different USDG price. USDG has six decimal places, so 1 is 0.000001 USDG and 1000000 is 1 USDG.
Keep prices as integer strings in atomic units. Do not use a decimal string such as "0.10" for priceAtomic.
Change the network or token
The template defaults to USDG on Robinhood Chain. To accept a different supported token, update network, asset, assetDecimals, assetName, and assetVersion together in src/config.ts. Confirm that the selected network and payment scheme are supported by the configured facilitator before deployment.
The payment middleware validates the buyer's payment before the handler returns the protected result and uses the facilitator to settle it.
Add more paid routes
Add each paid route to the paymentMiddleware configuration and define its matching Hono handler. Free routes such as /health need only a Hono handler.
paymentMiddleware(
{
"GET /weather": weatherPaymentTerms,
"POST /analysis": analysisPaymentTerms,
},
resourceServer,
)Keep authentication, request validation, rate limiting, and upstream API credentials in this generated server. Do not put secrets in route metadata or responses.
Check before deployment
bun run checkThis runs the TypeScript check and verifies the x402 challenge in memory. It does not spend funds and does not require the development server to be running.
To build and run the included Docker image:
docker build -t my-server .
docker run --env-file .env -p 4021:4021 my-serverAfter deployment, request the public protected URL without payment and confirm it returns 402 with a PAYMENT-REQUIRED header.
Then use an x402-compatible client to make one low-value paid request and confirm the response includes payment settlement metadata. The Buyer/Client Guide shows the client-side flow.
Production checklist
- Use an HTTPS
PUBLIC_URLthat matches the buyer-facing endpoint. - Restrict
CORS_ORIGINwhen the API is called from known browser applications. - Keep
.envand any upstream API credentials out of source control. - Add timeouts and input validation to your own service logic.
- Run a real low-value paid request before announcing the endpoint publicly.
- Monitor failed verification and settlement responses in production.