I wanted to sell a small digital product without building accounts, checkout pages, license-key emails, or a database of customers. The product was straightforward: a ZIP archive containing 300 English and Latin American Spanish game UI messages, plus JSON, CSV, Godot localization files, a validator, and a commercial license.
The awkward part was delivery. A download link alone is useful to a person, but an agent also needs to discover the product, inspect the price, pay, and retrieve the result without a manual checkout.
I ended up deploying the product as an x402 API on Cloudflare Workers. The finished service has four paid resources and several free discovery routes. This post records the decisions that mattered, including a few that were easy to miss.
Treat the archive as a build artifact
My first version could have read a ZIP from storage and returned it after payment. I chose a stricter path: generate the archive during the build, encode it into the Worker bundle, and publish its SHA-256 digest in the free catalog.
That gives the product a stable identity. The catalog currently reports an archive size of 211,351 bytes and this digest:
18d8efa856257a510e3fcc3da6e89dbd96d457b94c8dea0f27eae2cc76cde34c
The paid download response repeats the digest in an X-Content-SHA256 header. A buyer can compare what was advertised with what was delivered. It also makes accidental changes visible during deployment.
The build pipeline is deliberately boring:
{
"scripts": {
"generate:product": "node scripts/generate-product.mjs",
"check": "tsc --noEmit",
"test": "npm run generate:product && vitest run",
"build": "npm run generate:product && wrangler deploy --dry-run --outdir dist",
"verify": "npm run check && npm test && npm run build"
}
}
The generator runs before tests and before the deployment dry run. If the source files and the embedded archive drift apart, the build fails before anything reaches production.
Keep discovery free and make the paid surface small
The service exposes a free landing page, health check, sample, catalog, OpenAPI document, llms.txt, and x402 manifest. Payment is required only for these four routes:
| Route | Price | Result |
|---|---|---|
GET /v1/entry?key=main_menu.new_game |
0.25 USDC | One bilingual message |
GET /v1/category?category=main_menu |
2 USDC | One complete category |
GET /v1/bundle |
19 USDC | Complete JSON dataset |
GET /v1/download |
19 USDC | Complete ZIP archive |
This split solved two different problems. A developer can inspect the shape of the data before paying, while an agent can buy the smallest useful unit. The quarter-dollar entry is not meant to replace the full product. It is a low-risk way to test the payment and response flow.
The free catalog is live at the UX Words catalog. It includes the locales, category list, current prices, archive digest, payment network, asset, facilitator, and payout address.
Put the payment challenge in front of the handler
The Worker uses Hono with the x402 resource server and the exact EVM payment scheme. Each protected route declares its accepted price, network, recipient, description, and response type.
Here is the reduced shape of one resource:
paymentMiddleware(
{
"GET /v1/bundle": {
accepts: [
{
scheme: "exact",
price: "$19.00",
network: "eip155:8453",
payTo: PAY_TO,
},
],
description: "Complete bilingual game UI copy dataset",
mimeType: "application/json",
},
},
resourceServer,
);
An unpaid request never reaches the bundle handler. It receives 402 Payment Required and a PAYMENT-REQUIRED header describing the payment. After a valid payment signature is verified and settled, the request continues to the ordinary Hono route.
That separation is valuable. Product code only decides which bytes or JSON to return. Payment code decides whether the request may reach it.
The server does not need a wallet key
This was the most important security boundary for me. The Worker contains a public payout address, not a private key or seed phrase. The buyer signs the payment. The facilitator verifies and settles it. The server checks the result through the x402 flow.
Compromising the Worker bundle should not reveal a secret capable of moving the publisher's funds. There are still normal application risks, especially dependency and configuration mistakes, but the deployment is not a hot wallet.
I also made the payment details visible in the free OpenAPI document. Every paid operation includes fixed-price metadata, and the document names Base mainnet, native USDC, the recipient, and the facilitator. A client should verify those values against the runtime 402 response before signing anything.
Discovery metadata needs the same care as the API
It is tempting to treat OpenAPI and manifests as documentation generated after the real work. For an agent-facing service, they are part of the product.
The first discovery draft included free routes beside paid routes. A marketplace preview correctly treated those free routes as malformed payment resources. I changed the OpenAPI surface so it advertises exactly four paid operations, each with explicit payment metadata and response schemas. Free discovery remains reachable, but it is not misrepresented as something that accepts payment.
The current OpenAPI document is version 3.1 and lists only /v1/entry, /v1/category, /v1/bundle, and /v1/download under paths.
I also removed an early agent card after realizing that linking to a protocol-shaped document is not the same as implementing the protocol behind it. A missing claim is better than a false capability.
Test the challenge, not only the success path
The most useful production check costs nothing:
curl -i https://ux-words-x402.assorted-client-a65.workers.dev/v1/bundle
The expected result is status 402 with PAYMENT-REQUIRED. A 200 response would mean the product is exposed. A 500 response would mean the payment layer or facilitator initialization is broken. A 404 would mean discovery and routing disagree.
My tests also check that:
- the free catalog and sample stay public;
- all four paid routes return a payment challenge without a signature;
- the OpenAPI document contains exactly the four paid paths;
- every paid operation carries payment metadata;
- the product archive digest matches the generated bytes;
- the landing page publishes product structured data;
- obsolete protocol routes return 404.
The last check is easy to overlook. Removing a claim from documentation is not enough if the old endpoint still responds.
What I would repeat
For another small paid API, I would keep the same order:
- Build the product deterministically and publish a digest.
- Separate free inspection from paid retrieval.
- Declare a small number of paid resources.
- Put the payment middleware before business logic.
- Keep private wallet material out of the runtime.
- Make OpenAPI and discovery files describe only real capabilities.
- Test the unpaid 402 path on every deployment.
The live result is UX Words. The sample and catalog are free, while the complete product is 19 USDC on Base. Even if nobody buys the archive, the architecture is reusable: it turns a static digital good into a resource that both people and software can inspect before deciding to pay.
Top comments (0)