Sell a data lookup
Charge 0.001 SBC per request on testnet
In this tutorial you build a small API that sells one IP-reputation lookup for 0.001 SBC. You run it locally, see an unpaid request receive a payment challenge, and, after the buyer tutorial, see the payment arrive in your wallet. The data is synthetic; no external service is needed.
It takes about 10 minutes. For the same steps as a reference, see Accept payments.
Before you start
- Node.js 20 or later and pnpm.
- A wallet address to receive payments. It needs no funds and no private key on the server; see create and fund a wallet.
1. Create the project
mkdir lookup-seller && cd lookup-seller
pnpm init
pnpm add [email protected] [email protected]
pnpm add -D [email protected]hono serves the API, radius-sdk adds payments, and wrangler runs the Worker locally. The versions are pinned to the ones this tutorial was verified with; radius-sdk is pre-1.0, so newer minor versions can change its API.
2. Configure the Worker
Create wrangler.toml:
name = "lookup-seller"
main = "src/index.ts"
compatibility_date = "2026-09-01"Create .dev.vars with the address that receives payments, and keep it out of version control:
echo "PAY_TO=0xYourWalletAddress" > .dev.vars3. Write the API
Create src/index.ts:
import { Hono } from 'hono';
import { radiusPayments, type RadiusPaymentVariables } from 'radius-sdk/hono';
type Env = {
Bindings: { PAY_TO: `0x${string}` };
Variables: RadiusPaymentVariables;
};
const app = new Hono<Env>();
app.get('/health', (c) => c.json({ ok: true }));
app.use(
'/api/*',
radiusPayments<Env>({
network: 'testnet',
payTo: (c) => c.env.PAY_TO,
routes: {
'GET /api/lookup': { price: '0.001 SBC', description: 'Reputation lookup for one IP address' },
},
}),
);
app.get('/api/lookup', (c) => {
const ip = c.req.query('ip') ?? '0.0.0.0';
const payment = c.get('radiusPayment');
return c.json({ ip, reputation: 'clean', score: 7, paidBy: payment?.payer, transaction: payment?.transaction });
});
export default app;radiusPayments protects GET /api/lookup. The handler runs only after the payment has settled, so c.get('radiusPayment') always holds a payment there. /health has no price, so it stays free.
4. Run it
pnpm wrangler devThe Worker listens on http://localhost:8787. In another terminal, call the free route:
curl http://localhost:8787/health{ "ok": true }5. See the payment challenge
Request the lookup without paying:
curl -i 'http://localhost:8787/api/lookup?ip=9.9.9.9'The response is HTTP/1.1 402 Payment Required, with the offer in the PAYMENT-REQUIRED header. Decode it:
curl -s -D - -o /dev/null 'http://localhost:8787/api/lookup?ip=9.9.9.9' \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -dThe accepts entry says what a buyer must pay:
| Field | Value |
|---|---|
scheme | exact |
network | eip155:72344 (Radius testnet) |
amount | 1000: 0.001 SBC in base units (6 decimals) |
asset | The SBC contract |
payTo | Your PAY_TO address |
A request with an invalid payment gets the same 402, and the handler does not run:
curl -s -o /dev/null -w '%{http_code}\n' -H 'PAYMENT-SIGNATURE: eyJ4NDAyVmVyc2lvbiI6Mn0=' \
'http://localhost:8787/api/lookup?ip=9.9.9.9'4026. Get paid
Leave the Worker running and follow buy a data lookup with a second wallet. Then check your balance:
RADIUS_NETWORK=testnet npx [email protected] wallet balance 0xYourWalletAddress --jsonsbc increases by 0.001 for each paid lookup. The facilitator pays the gas, so you receive the full price.
What you built
- A route priced in SBC, with payment checked and settled before the handler runs.
- A free route next to it.
- A recipient address in configuration, with no key on the server.
Next steps
- Accept payments: per-request prices, reading the payment in handlers, settling after the handler, and moving to mainnet.
- Deploy: set the recipient with
pnpm wrangler secret put PAY_TO, then runpnpm wrangler deploy. - x402 payments: what happens between the challenge and the settlement.