| 1 | --- |
| 2 | title: 2.0.0-beta.55 - AI Gateway Spend Caps |
| 3 | date: 2026-06-11T20:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.55 adds `Cloudflare.AiGatewaySpendingLimit` — a declarative hard cap on account-wide AI Gateway spend — and re-exports the fetcher interop utils from `alchemy/Cloudflare/Bridge` so framework workers can wrap a service binding in a typed `HttpClient`. |
| 6 | --- |
| 7 | |
| 8 | An AI Gateway sits in front of your model calls, which means it's also |
| 9 | where a runaway loop turns into a runaway bill. beta.55 puts the |
| 10 | guardrail in your stack: **`Cloudflare.AiGatewaySpendingLimit`** sets a |
| 11 | hard dollar cap on cumulative AI Gateway spend across every gateway in |
| 12 | the account, declared and reconciled like any other resource. |
| 13 | |
| 14 | ## `Cloudflare.AiGatewaySpendingLimit` |
| 15 | |
| 16 | Amounts are in **cents** (Cloudflare's native unit — minimum `1_00` = |
| 17 | $1.00), accumulated over a `daily`, `weekly`, or `monthly` window with a |
| 18 | `fixed` (reset on the boundary) or `sliding` (rolling window) strategy: |
| 19 | |
| 20 | ```typescript |
| 21 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 22 | |
| 23 | // Cloudflare requires one manual credit top-up before a spending limit |
| 24 | // can be set. `topUp` reconciles that requirement: the provider observes |
| 25 | // the billing state and only charges the account's default payment |
| 26 | // method if the account has never topped up. |
| 27 | const cap = yield* Cloudflare.AiGatewaySpendingLimit("ai-spend-cap", { |
| 28 | amount: 250_00, // cents -> $250.00 (minimum 1_00 = $1.00) |
| 29 | duration: "monthly", |
| 30 | topUp: { amount: 10_00 }, // cents -> $10.00 (Cloudflare minimum) |
| 31 | }); |
| 32 | ``` |
| 33 | |
| 34 | The `topUp` prop deals with Cloudflare's bootstrap requirement: |
| 35 | an account can't set a spending limit until it has loaded Unified |
| 36 | Billing credits via at least one manual top-up (`NO_MANUAL_TOPUP`). |
| 37 | The provider observes `first_topup_success` on the live billing API |
| 38 | and only makes the one-time charge when the account has never topped |
| 39 | up — once bootstrapped, the prop is inert and never charges again. If |
| 40 | the payment needs interactive confirmation (3-D Secure), the resource |
| 41 | fails with a typed `AiGatewaySpendingLimitTopupRequired` error and the |
| 42 | top-up must be completed in the dashboard. |
| 43 | |
| 44 | Auto recharge is on by default — when the credit balance drops below |
| 45 | `threshold`, Cloudflare recharges by `amount` automatically: |
| 46 | |
| 47 | ```typescript |
| 48 | const cap = yield* Cloudflare.AiGatewaySpendingLimit("ai-spend-cap", { |
| 49 | amount: 250_00, |
| 50 | duration: "monthly", |
| 51 | topUp: { amount: 20_00, threshold: 10_00 }, // recharge $20 below $10 |
| 52 | }); |
| 53 | ``` |
| 54 | |
| 55 | Set `topUp: { amount: 10_00, autoRecharge: false }` for a one-time |
| 56 | bootstrap only. |
| 57 | |
| 58 | One caveat: Cloudflare stores a **single limit per account**, so this |
| 59 | resource is a per-account singleton — declare exactly one. Thanks |
| 60 | **[Matthew Aylward](https://github.com/Butch78)** |
| 61 | ([#569](https://github.com/alchemy-run/alchemy/pull/569)). |
| 62 | |
| 63 | ## Fetcher utils on `alchemy/Cloudflare/Bridge` |
| 64 | |
| 65 | The fetcher/socket interop helpers — `fromCloudflareFetcher`, |
| 66 | `toCloudflareFetcher`, `toHttpClient`, `fromCloudflareSocket` — are now |
| 67 | exported from the `alchemy/Cloudflare/Bridge` entrypoint, and |
| 68 | `fromCloudflareFetcher` / `fromCloudflareSocket` accept the global |
| 69 | `Fetcher` / `Socket` types directly |
| 70 | ([#581](https://github.com/alchemy-run/alchemy/pull/581)). |
| 71 | |
| 72 | That combination matters inside a framework worker (TanStack Start, |
| 73 | Astro, …), where your Effect backend arrives as a plain service binding |
| 74 | on `env`. Two calls turn it into a fully typed Effect `HttpClient` — |
| 75 | no casts: |
| 76 | |
| 77 | ```typescript |
| 78 | import * as Cloudflare from "alchemy/Cloudflare/Bridge"; |
| 79 | |
| 80 | const client = Cloudflare.toHttpClient( |
| 81 | Cloudflare.fromCloudflareFetcher(env.BACKEND), |
| 82 | ); |
| 83 | const res = await client |
| 84 | .get(`https://backend/?key=${encodeURIComponent(key)}`) |
| 85 | .pipe(Effect.runPromise); |
| 86 | return HttpServerResponse.toWeb( |
| 87 | HttpServerResponse.fromClientResponse(res), |
| 88 | ); |
| 89 | ``` |
| 90 | |
| 91 | The request rides the in-account service binding, not the public |
| 92 | network. The `cloudflare-website-tanstack-start` example's `/api/hello` route now |
| 93 | serves the same object four ways side by side — the raw R2 binding, |
| 94 | the service binding's `fetch`, typed RPC (`toRpcAsync`), and this |
| 95 | `HttpClient` form — each covered by an integration test. |
| 96 | |
| 97 | ## Also in this release |
| 98 | |
| 99 | - **Fixed a dangling process that kept `alchemy deploy` / `alchemy destroy` |
| 100 | from exiting** — local `RpcProvider` service layers (`providerServices` / |
| 101 | `providerServicesEffect`) now only construct when `AlchemyContext.dev` |
| 102 | is set, so one-shot CLI runs no longer spin up the local dev provider |
| 103 | machinery ([#580](https://github.com/alchemy-run/alchemy/pull/580)) |
| 104 | |
| 105 | ## Where to go next |
| 106 | |
| 107 | - [Add an AI Gateway](/cloudflare/ai/ai-gateway) |
| 108 | - [Frontend frameworks](/cloudflare/frontend/frontends) |
| 109 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta55) |
| 110 | - [Compare v2.0.0-beta.54 → v2.0.0-beta.55](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.54...v2.0.0-beta.55) |