x402 in practice: charge-per-call from zero to first payment
An end-to-end x402 tutorial — set up a paid endpoint with HTTP 402, charge 0.01 USDC per call, observe every payment with the open-source SDK, and verify settlement onchain. No Harpd account required to follow along.
This is the x402 tutorial I wish existed when we started. It walks from “what does HTTP 402 even mean” to “an agent paid my endpoint and I verified the settlement onchain” — in about thirty minutes, no Harpd account needed. Everything here uses open-source parts; the hosted layer is an upgrade, not a prerequisite.
What x402 is, in one paragraph
HTTP 402 (“Payment Required”) is a status code that has existed since 1991 and been unused since 1991. The x402 spec turns it into a real protocol: when a server wants money for a resource, it returns 402 with a payment challenge (an amount, a token address, a facilitator URL). The client signs a payment, retries with the payment in a header, and on a 200 the settlement has happened onchain. No card rails, no checkout flow, no merchant account. The agent holds its own keys.
What you’re going to build
A TypeScript endpoint that returns 402 until paid, and a TypeScript client that pays it. Both run on Node. Settlement is on Base Sepolia (testnet USDC, free). Every payment is logged by @harpd/observe. The whole thing is ~120 lines.
Step 1 — A wallet and some test USDC
You need a wallet with Base Sepolia USDC. Two options:
- Use Coinbase’s wallet-as-a-service if you’re running this for real
- Use any test wallet — get testnet USDC from a Base Sepolia faucet
You need the private key for the agent (it signs payments) and the public address for the server (it’s who gets paid).
Step 2 — The paid endpoint
// server.ts — Express + x402 middleware
import express from 'express'
import { x402Middleware } from 'x402-express' // hypothetical but the API is real
import { harpdPlugin } from '@harpd/observe'
const observe = harpdPlugin({
apiKey: process.env.HARPD_KEY!, // works in observe-only mode without Harpd
endpoint: 'https://api.harpd.com',
budgetControl: false, // server-side doesn't need budget control
})
const app = express()
app.use(x402Middleware({
payTo: '0xYourServerAddress',
token: '0xUSDC_base_sepolia',
amount: 10000, // USDC 6dp → $0.01 per call
chainId: 84532, // Base Sepolia
facilitator: 'https://facilitator.example.com',
hooks: observe.hooks, // ← every lifecycle event gets logged
}))
app.get('/premium-data', (req, res) => {
// If we got here, x402 already verified payment.
res.json({ data: 'here is your paid data, signed and settled' })
})
app.listen(3000)
The middleware intercepts every request to a paid route. If the request doesn’t include a valid payment header, it returns 402 with the challenge. If it does include one, it verifies the settlement onchain and forwards to your handler.
Step 3 — The paying agent
// agent.ts
import { x402Client } from 'x402-fetch'
import { harpdPlugin, defineBudget } from '@harpd/observe'
import { defineBudget as defineLocal, evaluate } from '@harpd/agent-budget-policy'
// Local budget — checked BEFORE the call, synchronously.
const localBudget = defineLocal({
perCall: { usdMax: 0.05 },
perAgent: { 'data-agent': { usd: 1, window: '1d' } },
perEndpoint: { 'http://localhost:3000/premium-data': { usd: 1, window: '1d' } },
})
// Observe-only instrumentation — every event goes to Harpd if you have a key,
// or to stdout if you don't. The SDK works either way.
const observe = harpdPlugin({
apiKey: process.env.HARPD_KEY ?? '',
endpoint: 'https://api.harpd.com',
budgetControl: true,
})
const client = new x402Client({
privateKey: process.env.AGENT_PRIVATE_KEY!,
facilitator: 'https://facilitator.example.com',
plugins: [observe],
})
async function fetchPremium() {
const intent = {
agentId: 'data-agent',
endpoint: 'http://localhost:3000/premium-data',
amountUsd: 0.01,
}
// 1. Local budget check — synchronous, never blocks on the network
const decision = evaluate(localBudget, intent)
if (!decision.allowed) {
console.log('deny', decision.reason)
return
}
// 2. observe's before-payment hook fires (logs intent)
// 3. x402 client signs & sends the payment
// 4. observe's after-settlement hook fires (logs result, verifies onchain)
const res = await client.fetch(intent.endpoint)
const data = await res.json()
return data
}
Step 4 — Run it
In two terminals:
# Terminal 1 — server
HARPD_KEY=... node server.ts
# Terminal 2 — agent
AGENT_PRIVATE_KEY=0x... HARPD_KEY=... node agent.ts
You should see the agent pay, the server’s after-settlement event fire as it verifies, and the response come back. If you have a Harpd key, the same events appear in your dashboard’s events table. If you don’t, they’re queued locally and flushed to stdout — the open-source layer works without Harpd.
Step 5 — Where to go from here
The pattern above is the whole shape. Once you have it:
- Change
amountto charge more or less per call. - Stack multiple paid routes behind one server; each gets its own
402challenge. - Add
budgetControl: trueto the agent’sharpdPlugincall — now Harpd also enforces the budget centrally, with team-wide policy editable from the dashboard. - Route
settlePaymentthrough Harpd for strict mode — the collector issues the idempotency key, dedupes retries, and writes the audit row.
Why this works without Harpd
Every part of the tutorial above is open source:
- The x402 middleware (or your own implementation following the spec)
- The x402 client library
@harpd/observe— runs in observe-only mode withapiKey: ''@harpd/agent-budget-policy— fully local
Harpd’s job is to host the parts that don’t make sense to embed in your agent: the dashboard, the multi-user policy editor, the cross-tenant marketplace scores, the alerts, the compliance export. You can run all of those yourself — they’re just a D1 database, a Hono worker, and a static site — but most teams would rather pay someone to host them.