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

  1. Compute the resource key from the request.
  2. If the request carries X429-Ticket, verify and redeem the ticket.
  3. If redemption is valid, spend the nonce and call the origin handler.
  4. If there is no ticket, check capacity.
  5. If capacity is available, call the origin handler.
  6. 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 keyUse when
GET /v1/inferOne endpoint is scarce.
POST /v1/search:{plan}Capacity differs by customer plan or class.
gpu:h100:region-euScheduling 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

MetricTypeWhy it matters
x429_capacity_admitted_totalCounterRequests admitted without a ticket.
x429_tickets_issued_totalCounterHow often the resource is saturated.
x429_tickets_redeemed_totalCounterDeterministic completions after overload.
x429_ticket_rejections_totalCounter by reasonSignature, scope, binding, expiry, replay, early window.
x429_redemption_lag_msHistogramDistance between promised notBefore and actual admission.
x429_paid_priority_totalCounterHow often priority was used and admitted.

Release checklist

  • /.well-known/x429.json returns the active issuer key.
  • Retry-After matches 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.

Read next