Skip to main content
Glama
README.md
# x402 Image API

**Paid AI image generation on the [x402 protocol](https://x402.org) — $0.001/image, USDC on Base, no API keys.**

> Live endpoint: **https://x402.chaly.org** · [health](https://x402.chaly.org/health) · [OpenAPI 3.1](https://x402.chaly.org/openapi.json) · [.well-known/x402](https://x402.chaly.org/.well-known/x402) · [llms.txt](https://x402.chaly.org/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.

## Buyer quick start

```bash
npm i @x402/fetch @x402/evm viem
```

```js
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`](./client.mjs) for a complete runnable client (including polling).

## MCP server

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

```json
{
  "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

```bash
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`](./cloudflared/README.md); parallel
launcher scripts in [`scripts/`](./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](./LICENSE)