Build: seller side
Server middleware
The server side turns a saturated resource into a deterministic offer: a free future slot and optional paid priority classes.
Install shape
import { x429, x429WellKnown, durableObjectNonceStore } from "@x429/hono";
import { mockPaymentVerifier } from "@x429/payment-mock";
app.use("/v1/*", x429({
issuer: "https://api.example.com",
kid: env.X429_KID,
privateKey: env.X429_PRIVATE_KEY,
publicKey: env.X429_PUBLIC_KEY,
docsUrl: "https://x429.dev/docs/",
resource: req => `${req.method} ${new URL(req.url).pathname}`,
capacity: { limit: 5, intervalMs: 1000 },
ticket: { freeDelayMs: 10_000, redeemWindowMs: 30_000 },
nonceStore: durableObjectNonceStore(env.X429_NONCES),
upgrades: [{
class: "immediate",
delayMs: 500,
price: { amount: "0.004", asset: "USDC", rail: "mock" },
payment: mockPaymentVerifier({ publicKey: env.MOCK_PAYER_PUBLIC_KEY })
}]
}));
app.get("/.well-known/x429.json", x429WellKnown({
issuer: "https://api.example.com",
kid: env.X429_KID,
publicKey: env.X429_PUBLIC_KEY,
docsUrl: "https://x429.dev/docs/",
paymentModes: ["mock"]
}));
Middleware decision order
- Compute the resource key from the request.
- If the request carries
X429-Ticket, verify and redeem the ticket. - If redemption is valid, spend the nonce and call the origin handler.
- If there is no ticket, check capacity.
- If capacity is available, call the origin handler.
- If capacity is exhausted, issue a signed free ticket and return an x429 offer.
Choosing a resource key
The resource key is the scheduling boundary. Make it too broad and unrelated endpoints block each other. Make it too narrow and agents can avoid congestion accounting.
| Resource key | Use when |
|---|---|
GET /v1/infer | One endpoint is scarce. |
POST /v1/search:{plan} | Capacity differs by customer plan or class. |
gpu:h100:region-eu | Scheduling a logical capacity pool rather than a URL. |
Nonce storage
Local demos can use in-memory storage. Production Workers should use a strongly consistent store such as a Durable Object.
export class X429NonceDurableObject {
async fetch(request) {
const { nonce, expiresAtMs } = await request.json();
const existing = await this.state.storage.get(nonce);
if (existing) return new Response("replay", { status: 409 });
await this.state.storage.put(nonce, expiresAtMs, { expirationTtl: 60 });
return new Response("ok");
}
}
Production notes
- Keep signing keys out of source control.
- Rotate keys by publishing multiple keys in discovery during the transition.
- Use conservative free delay windows until admission math is proven.
- Expose metrics for tickets issued, redeemed, expired, paid, and rejected.
- Log verification failures without leaking full ticket payloads.
Metrics to expose
| Metric | Type | Why it matters |
|---|---|---|
x429_capacity_admitted_total | Counter | Requests admitted without a ticket. |
x429_tickets_issued_total | Counter | How often the resource is saturated. |
x429_tickets_redeemed_total | Counter | Deterministic completions after overload. |
x429_ticket_rejections_total | Counter by reason | Signature, scope, binding, expiry, replay, early window. |
x429_redemption_lag_ms | Histogram | Distance between promised notBefore and actual admission. |
x429_paid_priority_total | Counter | How often priority was used and admitted. |
Release checklist
/.well-known/x429.jsonreturns the active issuer key.Retry-Aftermatches the free ticket wait.- Tickets are
Cache-Control: no-store. - Nonce storage survives concurrent redemption attempts.
- Origin handlers still enforce authentication and authorization. x429 is admission control, not auth.