A LangGraph pipeline that generates compiling Flutter apps (with a repair loop) A new LangGraph-based pipeline generates Flutter applications from JSON Product Requirements Documents, targeting zero-error compilation through a repair loop with four subagents and a QA gate running the real Flutter toolchain. The service, available as an HTTP MCP server at https://37-27-249-33.sslip.io/mcp, charges $3.00 USDC per build via x402, with free validation and payment terms endpoints. The pipeline is designed for machine-to-machine use, settling payments on-chain before builds start. Takes a JSON Product Requirements Document and emits a compiled Flutter application. Built as a LangGraph loop with four subagents, a QA gate that runs the real Flutter toolchain, and an x402 payment gate in front of packaging. The design goal is not "generates plausible code" but zero-error compilation : if the output fails CI, the loop has failed. Add this as an HTTP MCP server: https://37-27-249-33.sslip.io/mcp Four tools: validate prd and prd schema and payment terms are free — your agent can check whether a document is well-formed and see exactly what it would build before anything costs money. start build buys a build: call it once to get quoted, sign, call again to build. No account, no API key, no subscription — $3.00 USDC per build , paid on the spot via x402 https://x402.gitbook.io/x402/ and settled on-chain before the build starts. That's the whole pricing model: nothing recurring, pay only for the app you actually generate. Prefer curl? Same service, plain HTTP: Does my document build, and what would come out? Free. curl -X POST https://37-27-249-33.sslip.io/validate \ -H 'Content-Type: application/json' -d @examples/todo app.prd.json What does it cost, and how do I sign for it? curl https://37-27-249-33.sslip.io/.well-known/x402 /validate runs the same validator the paid path runs, so a document it accepts will not be rejected after payment. A build that fails still returns its diagnostics, because payment settles first and a silent failure would be theft. No credentials and no Flutter SDK needed — the defaults are fully offline: poetry install poetry run python src/supervisor.py examples/todo app.prd.json --clean That validates the PRD, runs the loop, and writes a Flutter project to generated apps/current build/ . To grade the output with the real toolchain instead of the offline stub: poetry run python src/supervisor.py --clean --analyzer dart --flutter-root "C:\flutter" --run-tests CLAUDE.md calls this an M2M microservice; this is the part a machine buyer calls. X402 SHARED SECRET=... FLUTTER ROOT="C:\flutter" poetry run uvicorn src.service.app:app --port 8000 | Endpoint | | |---|---| POST /builds | PRD in; 202 + job id, or 402 with an x402 challenge | GET /builds/{id} | status, log, diagnostics | GET /builds/{id}/apk | the artifact | GET /healthz | includes whether payment is configured | Builds take minutes, so the API is asynchronous. x402 payment verified in a submitted PRD is discarded : the PRD is buyer-supplied, so trusting that field would let anyone assert their own payment. Only the server's verifier sets it. Payment is real x402: the buyer signs an EIP-3009 TransferWithAuthorization EIP-712 , and the service recovers the signer, checks recipient, amount, chain and validity window, and claims the nonce atomically so one signature buys exactly one build. X402 TOKEN CONTRACT=0x036CbD... X402 CHAIN ID=84532 X402 PAY TO=0xYourAddress X402 PRICE ATOMIC=500000 poetry run uvicorn src.service.app:app --port 8000 Configure nothing and the service refuses every payment — it fails closed rather than falling back to the development shared secret. /healthz reports which mode is active. Verification is not settlement. A valid signature proves the payer authorised a transfer; it does not prove they hold the balance or that the transfer landed on-chain. Set X402 FACILITATOR URL and the service submits the authorization for on-chain execution and blocks the 202 until it confirms — so a build only starts once the money has actually moved. Without it, /healthz reports settlement: verification-only and the service is accepting signed promises. Before the nonce is claimed the service asks the facilitator's /verify endpoint whether the payment would settle. Insufficient funds is recoverable — the payer tops up and re-presents the same signature — so burning their authorization for it would be needlessly destructive. Settlement distinguishes three outcomes, not two. A refusal "insufficient funds" is definite and is not retried. A timeout is unknown : the facilitator may have broadcast and failed to answer, so the build is refused while recording that the buyer may have been charged. Retrying a transport failure is safe — an EIP-3009 nonce is single-use on-chain, so a duplicate submission reverts rather than charging twice. Verified end to end on Base Sepolia — a real transaction, a real APK: export X402 TOKEN CONTRACT=0x036CbD53842c5426634e7929541eC2318f3dCF7e USDC export X402 CHAIN ID=84532 export X402 NETWORK=base-sepolia export X402 PRICE ATOMIC=3000000 3.00 USDC export X402 PAY TO=0xYourReceivingAddress export X402 FACILITATOR URL=https://facilitator.payai.network public, no auth export FLUTTER ROOT="C:\flutter" poetry run uvicorn src.service.app:app --port 8000 The payer needs USDC only — no ETH . The facilitator submits the transaction and pays gas, which is the point of EIP-3009; if a wallet is being asked for testnet ETH, something is wired wrong. Faucet: https://faucet.circle.com https://faucet.circle.com . scripts/check x402 funding.py reports whether a payer is funded by asking the facilitator's /verify , rather than introducing a second source of truth that could disagree with the one the gate actually uses. Set REDIS URL and both the nonce store and the job store move to Redis; /healthz reports multi process safe . Nonce consumption uses SET NX EX , one atomic round trip, so the time-of-check/time-of-use protection holds across workers rather than only within one interpreter. A Redis outage refuses payment rather than allowing replays. The money moves on-chain before the build starts, so losing a build to a deploy means having taken payment for nothing. Execution is therefore durable, not just job state : POST /builds settles the payment, writes the PRD onto the job record, and pushes the id onto a queue. Workers pull from it. BUILD WORKER EMBEDDED=0 poetry run uvicorn src.service.app:app --port 8000 accepts poetry run python -m src.service.worker builds An API process builds as well as accepts by default, so a single-container deployment needs neither of those flags; set BUILD WORKER EMBEDDED=0 once requests and builds want scaling apart. A worker holds a lease on the job it is building and renews it while it works. Kill the worker and the lease lapses, another worker returns the job to the queue, and it is rebuilt from the stored PRD — which is why the PRD is stored rather than captured in a closure, as it was when execution lived inside the accepting process. scripts/verify durable execution.py demonstrates exactly that, across real processes and a real SIGKILL . Two things this deliberately does not promise. A lease bounds duplicate work rather than preventing it: a worker partitioned from Redis for longer than its lease is indistinguishable from a dead one, so its build may be handed on while it is still running. renew returning False is how the stalled worker learns to discard its result, but the overlap is real, and it costs compute rather than money — the payment settled once, before the job was queued. And retries are bounded BUILD MAX ATTEMPTS , default 3 : without that, one build that reliably kills its worker would be requeued into the next one and take the fleet down a process at a time. | Variable | Default | What it is for | |---|---|---| BUILDS RATE LIMIT | 20 | POST /builds per address per window; 0 disables | BUILDS RATE WINDOW SECONDS | 60 | the window that limit applies over | BUILD WORKER EMBEDDED | 1 | whether the API process also builds | BUILD LEASE SECONDS | 300 | how long a silent worker keeps its job | BUILD HEARTBEAT SECONDS | 30 | how often a working worker says so | BUILD MAX ATTEMPTS | 3 | before a build is given up on for good | /healthz reports durable execution , which is true only when the queue and the job store are both shared — a Redis queue over an in-memory job store loses the record the build would be resumed from. The x402 gate stops anyone getting a free build, so this is not about theft. It is the cost of refusing : POST /builds carrying a payment header makes the service call the facilitator's /verify and then /settle , two network round trips with a 60-second timeout, from a synchronous endpoint. A few hundred concurrent requests with junk authorizations exhaust the thread pool and the service stops answering anyone — including the buyers who paid. So the limit is on that endpoint only; polling a running build and downloading an APK are cheap and legitimately frequent, and throttling them would punish correct behaviour. Identifying the caller is the part that fails quietly. Behind the TLS proxy every request arrives from Caddy's address on the compose network, so keying on the socket peer puts every buyer in the world into one bucket — a limiter that looks configured while blocking either everyone or nobody. The client is the rightmost X-Forwarded-For entry, because Caddy appends the peer it actually saw to whatever the caller sent. Taking the leftmost, which is the more common convention, would let a caller supply their own header and mint a fresh identity per request; there is a test that fails on exactly that. One container that accepts payment and builds APKs, plus the Redis that makes both durable. It wants a VM with a disk: the image carries Flutter, the Android SDK and a JDK, and measures 7.71 GB , which rules out serverless targets and most PaaS free tiers. A finished build used to cost 2.0 GB of Gradle output for a 144 MB APK, and nothing reclaimed it — job records expire from Redis after seven days, which is the wrong half, since the record is kilobytes and the directory it names is gigabytes. The service now keeps the artifact and drops the tree the moment a build finishes, while the worker still holds the lease. Measured on the deployment: the last unpruned build is 2.0 GB on disk, the first pruned one is 144 MB, and it still downloads. That is the difference between about 58 sales and about 790 on a 150 GB box. cp .env.deploy.example .env.deploy fill in X402 PAY TO and ANTHROPIC API KEY docker compose --env-file .env.deploy up -d --build poetry run python scripts/verify deployment.py http://127.0.0.1:8000 docs/DEPLOY.md is the runbook: how the machine should be sized and why, the firewall trap that Docker's published ports set for ufw, and the order — build, prove over an SSH tunnel, buy one real build, and only then put it on a public address behind TLS. Every toolchain version in the Dockerfile is pinned to the one the table above was proven against — Flutter 3.44.8, Android SDK 36, build-tools 36.0.0, JDK 17. Floating any of them makes the build environment a moving target, which is the one thing a service promising "it compiles" cannot afford. tests/test deployment config.py fails if they drift, if a ${VAR} in compose has nothing to supply it, or if the builds volume disappears. The deployment check is the point of that last command. A container can come up, answer HTTP and still be unsellable: taking payments it never settles, losing paid builds on restart, or falling back to the dev shared secret because a token variable was misspelled. /healthz was built to make each of those visible in one line, and verify deployment.py refuses the deployment when it sees them. It was itself checked against four deliberately broken deployments — no Redis, no facilitator, no token config, wrong chain, no rate limit — and caught all five with the fix named in the message. --pay goes further and buys a real build with the testnet key, which is the only check that proves the deployment can do the thing it charges for. Two things about the buyer-facing surface are worth knowing before pointing traffic at it. The service defaults to the Claude generator, so a deployment without ANTHROPIC API KEY accepts payment and then fails to build; SUPERVISOR GENERATOR=template is the free, deterministic path and a sane first deploy. And SUPERVISOR BUILD MODE decides whether a buyer gets an installable debug APK or an unsigned release one — see below for why the service will not sign. The image has now been built and sold from. A 4 vCPU / 8 GB Hetzner box running Ubuntu 26.04 built it on the first attempt, and a signed EIP-3009 authorization settled on Base Sepolia 0x0637c94b… https://sepolia.basescan.org/tx/0x0637c94b490531065d3476147eaa7484ed2565ad8168b0639d21cf63b1edb2a5 bought a 151 MB APK from it end to end. It did not work first time, and the way it failed is the point. Every layer of the image built, the service came up healthy, /healthz reported a correct posture, and the deployment check passed — and then the first paid build died: PermissionError: Errno 13 Permission denied: '/opt/flutter/bin/flutter.bat' The Flutter SDK ships flutter and flutter.bat on every platform. Four call sites had independently resolved the tool by taking the first candidate that exists , Windows spelling first — correct here, an unrunnable batch file on the machine that was actually going to run it. 299 tests, an eval sweep, APK packaging and the entire verification table had all passed on Windows, where the bug cannot appear. It took a buyer paying for a build to find it, which is exactly the failure mode the paid check exists to provoke while the buyer is still us. src/ports/toolchain.py now answers that question once, and takes windows= as a parameter so both branches are reachable from a test on either platform. flutter create scaffolds a release build type that signs with the debug key, under a // TODO: Add your own signing config comment. Left alone, flutter build apk --release succeeds, produces a plausible app-release.apk , installs on a device, and cannot be published — Play rejects the debug key. A buyer would find that out at upload, having already paid. So --build-mode release strips that config and emits an unsigned APK. poetry run python src/supervisor.py