A self-hosted class booking server with x402 micropayments. Reading the schedule costs nothing; $0.05 buys a seat and the payment returns the seat number, the full class details, a calendar invite and a signed QR pass the door can scan — all in the same HTTP response. Pay in USDC on Base or Solana — your client picks the rail.
| Route | Price | The artifact you get back |
|---|---|---|
GET /schedule | free | The class calendar with live seat counts — sessions, instructors, rooms, seatsLeft, enrollmentOpen |
POST /enroll/:classId | $0.05 | {seatNumber, classDetails, ics (base64 invite), pass: {token, verifyUrl, qrSvgDataUri, expiresAt}, signature} |
GET /verify/:token | free | Pass validation for the door — valid, payload, check-in status, session |
POST /check-in/:token | free | Marks a pass used; re-scans report alreadyCheckedIn |
GET /roster/:classId | free | Seats sold: seat number, holder name, check-in status |
GET /.well-known/x402 | free | Machine-readable payment manifest for agents and indexers |
Every paid route returns the thing you bought in the 200 body. No pay-now-deliver-later.
POST /enroll/:classId with no payment. Server replies 402 with an accepts[] holding both rails — amount, token address and recipient on Base and on Solana.
Base: sign an EIP-3009 USDC authorization. Solana: sign an SPL transferChecked. Retry with the X-PAYMENT header; that rail's facilitator verifies and settles on the chain you chose.
200 with the seat, the invite and the QR pass in the body and a settlement receipt in X-PAYMENT-RESPONSE, naming the rail. Settlement only runs on success.
import { wrapFetchWithPayment } from "x402-fetch";
import { privateKeyToAccount } from "viem/accounts";
const payFetch = wrapFetchWithPayment(fetch, privateKeyToAccount(PRIVATE_KEY));
// the schedule is free — no wallet needed to browse
const { sessions } = await fetch("http://localhost:4023/schedule?days=7").then(r => r.json());
const session = sessions.find(s => s.seatsLeft > 0 && s.enrollmentOpen);
const ticket = await payFetch(`http://localhost:4023/enroll/${session.classId}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "Agent Ada" }),
}).then(r => r.json());
console.log(ticket.seatNumber, ticket.pass.verifyUrl, ticket.pass.qrSvgDataUri);
Studio-style flow with the drop-in payment modal — browse the free timetable, tap a class, pay with Phantom (Solana) or MetaMask (Base), get the QR pass on screen.
public/index.html →skill.md + .well-known/x402 discovery, MCP tool wrapper for Claude, listing on x402scan / Bazaar.
agents.md →Every endpoint with params, prices, schemas, and error cases. OpenAPI 3.1 included.
api.md →