Denial of Wallet is the class of bug that costs you money instead of data. A render loop, a missing idempotency key, a retry storm, an unbounded fan-out, or a serverless function that re-triggers itself. None of them leak a single record. All of them can turn a quiet Tuesday night into a five-figure cloud bill before you've finished your coffee. IOnclad's CostGuard scanner hunts for these patterns directly in your code.
Most security tools only care about who gets in. CostGuard cares about what your own code does to your bill when nobody is watching. It checks JavaScript and TypeScript for seven distinct money-drain shapes, ranked by blast radius:
A leaked key gets rotated and the bleeding stops. A wallet-drain bug keeps charging your card every second until you find it. Here are the patterns, from worst blast radius down.
A React useEffect with a network call and no dependency array runs on every render. If it also sets state, each render fires another fetch, which sets state, which renders again. An infinite loop hammering your API within seconds of the page loading. This is the single most common AI-generated cost bomb, which is why CostGuard ships it at High. An empty [] deps array (mount once) is correct and never fires.
A while(true) or for(;;) loop that retries a network or paid-API call inside a catch with no backoff, no attempt cap, and no break. One persistent 500 from the upstream service spins this at full speed: thousands of billable calls per second, plus a thundering-herd hit that can keep the dependency down. CostGuard only flags genuinely uncapped loops. A real attempt counter (attempt < 5), a break, or a retry library like p-retry clears it.
A Firestore trigger fires on a collection, then writes back to the same collection. The write re-fires the trigger, which writes again. Infinite recursion. This exact pattern is the well-known cause of overnight five-figure Firebase and GCP bills. CostGuard catches both the named-collection self-write and the v2 ref-style form (event.data.after.ref.update) that targets the same doc without naming the collection.
A Stripe charge, payment, or subscription created with no idempotency key double-bills the customer on any retry: a network blip, a double-click, an at-least-once queue. A webhook handler that writes without de-duping on the event id re-processes the same event, because Stripe, GitHub, and Svix all deliver at-least-once. And a Promise.all(items.map(...)) with no concurrency cap launches one paid call per element, so a big array means hundreds of simultaneous calls, instant rate-limit bans, and a metered spike.
Denial of Wallet does not need an attacker. The render loop, the recursive trigger, and the uncapped retry all fire from your own users on a normal night. But CW07 is the one that invites an attacker in: a metered API (LLM, SMS, email, maps, search) reachable with no auth means anyone on the internet can script your endpoint and run your bill up on purpose. That's economic denial-of-service, and it's free for them.
Two lines of code, one missing argument, a bill nobody approved:
// The component mounts. The effect has no deps array. useEffect(() => { fetch('/api/recommend').then(r => setItems(r)); // setItems re-renders... }); // ...which re-runs this. // Every render fires another paid call. The loop never stops. // By morning: 4.1M requests to the recommendations API. useEffect(() => { fetch('/api/recommend').then(r => setItems(r)); }, []); // runs once. fixed.
CostGuard is a dedicated cost-and-resilience scanner that runs 100% offline. It reads your JavaScript and TypeScript, never sends it anywhere, and is built to stay quiet on safe code:
Promise.all fan-out, self-triggering serverless functions, and metered APIs left open to anonymous callers.useEffect deps, capped-and-backed-off retries, idempotency keys, and concurrency limiters (p-limit, concurrency:) are recognized as correct and stay silent.CostGuard finds known patterns and common misconfigurations. It is not a guarantee or a load test, and LaunchGuard guidance is advisory, not legal or financial advice.
[] to run once on mount, or [id] to refetch only when id changes. Never leave the second argument off when the effect calls an API.p-retry / axios-retry with a retries bound. Never retry in an unbounded loop.pLimit(5) or process in chunks, sized to what the downstream rate limit allows.processed flag, or check the field actually changed before writing back.@upstash/ratelimit, @arcjet), and set a hard spend cap at the provider so a bug or an attacker can't run past a ceiling.Scan your code and full git history in under a minute. Free verdict, no signup, nothing leaves your machine.