A Laravel app that generates images, video, and text through [Runware.ai](https://runware.ai)'s API, with every generation's real cost tracked and a hard monthly spending cap enforced before any request goes out.
It's the shared foundation for three ad-monetized products (an SEO image gallery site, a free viral tool, a faceless Shorts channel) that will sit on top of the same Runware integration — but on its own it's a working generator: submit a prompt, pick a type, get an image/video/text result, and always know exactly what it cost.
Image and text generation run synchronously in the request. Video is different — Runware generates it asynchronously, so submitting a video prompt queues `App\Jobs\GenerateVideoJob`, which submits the task, polls Runware's `getResponse` endpoint until it's done, downloads the result, and records the final cost. All generated files are downloaded and stored on local disk (`storage/app/public`, served via the `public` symlink) rather than left on Runware's CDN.
- A configurable hard cap in EUR (`RUNWARE_MONTHLY_CAP_EUR`, default 20) resets every calendar month.
- Every request is checked *before* it's sent: current month's spend + this request's estimated cost + a safety margin must stay under the cap, or it's rejected with no API call made.
- The check-and-reserve step is protected by an atomic cache lock, so two requests arriving at the same time can't both slip through right at the boundary.
- Runware bills in USD; conversion to EUR uses a live rate fetched daily (`CurrencyRateService`, via the free Frankfurter API) and cached, falling back to a configured static rate if the fetch ever fails — the cap never stops enforcing just because a rate refresh failed.
Runware's model-search API doesn't return pricing, so there's no way to discover "the cheapest model" from their API directly. Instead, models are entered locally (AIR id, label, unit, USD price) through the `/runware-models` admin page, and the cheapest *active* model per type is automatically flagged as the default whenever a model is added, updated, or removed — no manual "set default" step.
Laravel 13, MySQL in production (a local sqlite fallback is used when the shared dev DB is unavailable — see `.env`), Blade + Tailwind (no frontend framework), local disk storage for generated media, database-backed queue for the video job.
Set `RUNWARE_API_KEY` in `.env`, then add at least one model per type at `/runware-models` (prices from [runware.ai/pricing](https://runware.ai/pricing)). Run `php artisan queue:work` for video generations to actually process.
Runware calls are mocked with `Http::fake()` in `tests/Feature/GenerationServiceTest.php` and `RunwareModelDefaultTest.php` — no API key needed to run the suite.