A self-hosted live waitlist for walk-in lists, counters and service desks. Seeing the wait is free; $0.01 holds your place and hands back a signed position token and a signed refund claim that matures automatically if you are never served. Checking your position afterwards is free, forever. Pay in USDC on Base or Solana — your client picks the rail.
| Route | Price | The artifact you get back |
|---|---|---|
GET /queues | free | Every queue with its live length, estimated wait, capacity and whether it is accepting joins |
GET /queues/:id | free | Live board for one queue — everyone in order, anonymised to an initial |
POST /join | $0.01 (refundable) | {ticketId, token, position, ahead, eta, refundClaim, positionUrl, signature} |
GET /position/:token | free | Live position, ETA, serve status and refund status. Pollable — you paid for the place, not the look |
POST /claim/:token | free | Signed refund record. Idempotent, authenticated by the token itself |
POST /leave/:token | free | Give up your place; the hold is returned and the queue closes up behind you |
Every paid route returns the thing you bought in the 200 body. No pay-now-deliver-later.
POST /join 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 signed token and refund claim 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));
// seeing the wait is free — no wallet needed
const { queues } = await fetch("http://localhost:4025/queues").then(r => r.json());
const q = queues.find(x => x.acceptingJoins && x.estimatedWaitMinutes < 30);
// $0.01: hold the place, get the token + refund claim back immediately
const ticket = await payFetch("http://localhost:4025/join", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ queue: q.id, name: "Agent Ada", party: 2 }),
}).then(r => r.json());
console.log(ticket.position, ticket.eta.minutes, ticket.refundClaim.claimableAfter);
// checking is free — poll it as often as you like
const pos = await fetch(ticket.positionUrl).then(r => r.json());
console.log(pos.status, pos.position, pos.refund.due);
Live-board flow with the drop-in payment modal — watch the wait for free, join with Phantom (Solana) or MetaMask (Base), and the position ticks down 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 →