Running a pay-per-call API platform on Cloudflare's free plan: what the limits taught me
AIPayGen is my side project: a platform where AI agents pay per call for tools (DNS lookups, crypto prices, page-to-Markdown, weather and about 90 more), ask their owner for approval, and run routines on a timer. Disclosure: I built it, and it's early. There's no real revenue yet. The first version ran on a Raspberry Pi 5 on my desk: Flask, SQLite and a Cloudflare tunnel. This version is a rebuild with one rule: hosting costs $0. Here's how that works and which limits actually mattered. The stack One Cloudflare Worker (TypeScript, Hono) serves the website, the HTTP API, an MCP server, a small phone app and the cron jobs. D1 (Cloudflare's SQLite) holds keys, credits, the billing log, the marketplace catalog, watches and schedules. Workers AI runs open models (Llama 3.3 70B for the assistant, smaller ones for text tools). x402 handles payments: an unpaid call gets HTTP 402 with the price, and the agent retries with a signed USDC payment on Base in a header. No account needed. Prepaid credits on an API key work too. Limit 1: rows written, not CPU I expected the 10 ms CPU limit per request to hurt. It didn't. Most work is waiting on fetches, which doesn't count. What hurt was D1's 100,000 rows written per day. I hit it once, and every write failed until midnight UTC. The surprises: Indexes multiply writes. Updating a row also rewrites every index entry that covers a changed column. A last_checked_at column with an index cost two or three writes per check. Full-text search is expensive. Re-upserting a catalog entry deleted and re-inserted its FTS row even when nothing changed. Now the upsert skips FTS unless the name, description or category changed. Rotating by primary key beats a "last checked" column. The watch runner keeps one cursor in a small key-value table and scans the next slice by id. That's one write per run instead of one per row. // Next slice of watches after the cursor, wrapping to the start. const rows = await db.prepare( "SELECT * FROM watches WHERE id > ? ORDER BY id LIMIT ?" ).bind(cursor, perRun).all(); Rule of thumb: before adding a column you update often, count the writes it costs per day. Limit 2: 50 subrequests per request Composite tools (a "wallet report" that calls a sanctions check, balances and labels) can fan out fast. Each sub-call gets a deadline and a budget, and a report that can't run its safety check fails instead of returning half an answer. The caller isn't charged for a failure. Limit 3: some APIs block Cloudflare's IPs A few public APIs return 429 or 402 to every request from Workers IPs. A tiny relay on another free tier fixed some of them. For the rest, the tool is marked down rather than left to fail at random. Charging only for calls that worked x402 splits payment into verify and settle. The Worker verifies the signed payment, runs the tool, and settles only if the tool succeeded. A failed call never moves money, so there are no refunds to handle. The same split powers a gateway for sellers: point it at your HTTPS API and get a pay-per-call URL. It settles only when your API returns 2xx, and it pins every request to your base path. Decoding the path before checking it matters: %2e%2e is ... The owner side The part I use myself: ask_human: the agent asks me a question with buttons, and the push reaches my phone. schedule_create: "news on these topics every morning", delivered to the app. watch_create: site down, page changed, domain expiring, price crossed. One key per agent, each with a daily spending limit. Web push from a Worker works without a library: a VAPID JWT signed with WebCrypto (ES256) and a payloadless push. The service worker then fetches the inbox itself. Try it curl -X POST https://aipaygen.com/v1/dns_lookup \ -H 'content-type: application/json' -d '{"domain":"example.com"}' MCP: https://mcp.aipaygen.com/mcp · docs: aipaygen.com/docs If you've run something real on D1's free plan, I'd like to hear what limit you hit first.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to