x402-image-api
by Calivvvv
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)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues