Skip to main content
Glama

x402 Image API

Paid AI image generation on the x402 protocol — $0.001/image, USDC on Base, no API keys.

Live endpoint: https://x402.chaly.org · health · OpenAPI 3.1 · .well-known/x402 · llms.txt

The x402 standard revives HTTP 402 Payment Required for machine-native payments: a buyer (human or AI agent) POSTs a prompt, receives a signed payment offer in the PAYMENT-REQUIRED header, authorizes an EIP-3009 USDC transfer with their wallet, retries the request, and a third-party facilitator verifies + settles onchain. No accounts, no API keys, no gas for the buyer — the payment IS the auth.

POST /generate {prompt}          → 402 + PAYMENT-REQUIRED (machine-readable offer)
   ↳ buyer signs EIP-3009 authorization (USDC on Base)
POST /generate {prompt} + PAYMENT-SIGNATURE
                                 → 202 {jobId, pollUrl, estimatedSeconds}
GET  /result/:id every ~5s       → processing … → done {urls: [...]}

Pricing

quality

megapixels

per image

low

1K

$0.001

  • Total charged = per-image × count (count 1–8, or batch items: [["prompt", n], …], total ≤ 8).

  • Aspect ratio is free.

  • Bigger tiers exist in the internal price table but only servable tiers are advertised.

  • Pricing is dynamic: the 402 offer is computed per request from the body (PRICE_TABLE in server.js), so what the buyer is asked to pay is exactly what the handler generates.

Related MCP server: Nexus MCP

Buyer quick start

npm i @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.BUYER_PRIVATE_KEY);
const fetchWithPay = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
});

// 402 → auto-pay → retry → 202 {jobId, pollUrl}
const res = await fetchWithPay("https://x402.chaly.org/generate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ prompt: "a red cube on a blue table", count: 1 }),
});
const job = await res.json();
// poll GET job.pollUrl every 5s until status === "done"

See client.mjs for a complete runnable client (including polling).

MCP server

mcp-server.mjs exposes the API as MCP tools (image_generate, image_health, image_openapi, image_poll) so coding agents can generate images directly:

{
  "mcpServers": {
    "x402-image-api": {
      "command": "node",
      "args": ["mcp-server.mjs"],
      "env": {
        "X402_API_URL": "https://x402.chaly.org",
        "BUYER_PRIVATE_KEY": "0x…"
      }
    }
  }
}

Unpaid tools (health/openapi/poll) work without a key.

Self-hosting

npm install
cp .env.example .env      # set PAY_TO_ADDRESS (public address only!)
npm start                 # node --env-file=.env server.js → :4021

Key env vars: PAY_TO_ADDRESS, X402_NETWORK (eip155:8453), X402_FACILITATOR_URL (e.g. PayAI), PORT, OUT_DIR, PUBLIC_URL, MAX_COUNT, plus MEDIA_IO_PROJECT_DIR — path to your generation backend.

Pluggable backend: the Express layer shells out to a one-shot worker subprocess per request and reads a single JSON line from stdout. This repo ships the seller side; our own bridge worker (gen_worker.py) is kept private. To plug in your own generator, provide an executable that

  • reads {prompt, aspect, megapixels, quality, count, …} as JSON on stdin,

  • writes {ok, urls: [...], …} as the last stdout line,

  • exits non-zero on failure.

Public exposure (no VPS)

A Cloudflare Tunnel is the only ingress — TLS terminates at Cloudflare's edge and a downed origin serves 503/530 automatically (free heartbeat). Full walkthrough in cloudflared/README.md; parallel launcher scripts in scripts/ (+ systemd units for Linux).

Design notes

  • Middleware order matters: express.json() runs before the payment middleware so the dynamic-price function can read the parsed body — otherwise a $0.008 batch silently quotes as one $0.001 image.

  • Capacity gate before payment: a pre-payment middleware returns 503 insufficient_capacity when no generation slot is claimable today, short-circuiting before any 402 — buyers never sign for something we can't deliver.

  • 202 + polling instead of long-lived responses: generation takes ~50–90 s/image; returning a job handle immediately avoids gateway timeouts (Cloudflare's 100 s limit) for batches up to 8 images.

  • White-label output: PNGs land under GET /images/:file (hash names, traversal-guarded) served from your domain.

  • Bazaar discovery: declareDiscoveryExtension exposes this service in the x402 Bazaar; agents can find it without being told the URL.

  • Idempotency: pass "id" to dedupe retries — cached results are returned without re-generation (orders.jsonl + in-memory jobs).

Security

  • The server only ever holds your public receiving address; settlement is facilitated onchain and lands in your wallet.

  • .env, jobs.json, orders.jsonl are gitignored — never commit them.

  • Use a dedicated wallet for PAY_TO_ADDRESS; keep treasuries cold.

License

MIT

Related MCP Connectors

Related MCP Servers