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 amount to charge more or less per call.
  • Stack multiple paid routes behind one server; each gets its own 402 challenge.
  • Add budgetControl: true to the agent’s harpdPlugin call — now Harpd also enforces the budget centrally, with team-wide policy editable from the dashboard.
  • Route settlePayment through 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 with apiKey: ''
  • @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.

Next