GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/Advanced
Advanced/2026-05-10Advanced

Turning my own artworks into 120 wallpaper variants in 30 days with Gemini 3.2 Pro and Imagen 4 — an artist-developer's content supply pipeline

A 30-day production log of building a pipeline that takes one of my own paintings and expands it into a 120-piece wallpaper series, using Gemini 3.2 Pro for structural analysis and Imagen 4 for variations. Includes real costs, quality gates, and downstream app KPIs.

Gemini 3.2 ProImagen 4indie dev5wallpaper appsproduction140art2Python38

I have been shipping mobile apps as a solo developer since 2014. Once cumulative downloads crossed fifty million, the heaviest part of running them quietly became the same thing every month: refilling the catalog. Wallpaper apps are the most unforgiving here. The day fresh material stops landing, daily active users drop. For years I held the line by buying stock and painting some of the work myself, but in early 2026 — with Gemini 3.2 Pro and Imagen 4 finally lining up inside the same Google AI family — I decided it was finally time to build a serious pipeline that starts from one of my own paintings and produces a full wallpaper series.

This post is the production log from late March through April 2026. I ran it for thirty days, generated 168 candidates from twelve source paintings, and ended up with 120 wallpapers that actually shipped to my apps. Below is the full pipeline, with code, the quality gate that brought my acceptance rate from 49% to 71%, the safety-filter pitfalls that quietly burned a half day before I caught them, and the cost and KPI numbers I have been measuring against.

For context, the source artworks are abstract pieces I have been making for the last few years, focused on the layered Japanese sense of prayer and collective consciousness. I have been fortunate enough to receive seventeen international art awards on that body of work, and people kept asking whether the same world could live on a phone home screen. I had been wanting to find out for myself, and I wanted the answer to come from running the system in production rather than from a conceptual essay.

The constraint only an artist-and-developer hits

If you paint, you will recognize the first problem immediately. A painting is composed for the canvas it was made on, and slicing a 4:5 oil composition into a 9:19.5 phone aspect ratio destroys whatever balance the original had. The visual weight that worked when the canvas was square gets shoved to one corner, and the negative space — which in my case is doing a lot of the meaning — collapses. The second problem is resolution. Most of my older work only exists as web-grade JPEGs uploaded to galleries and social media, which does not survive a Retina screen, let alone the 1290×2796 pixels an iPhone Pro Max wallpaper actually needs.

I could outsource cropping or repainting. The going rate I was getting from outside collaborators sat between 8,000 and 15,000 yen per finished wallpaper, and I needed somewhere between 30 and 50 new pieces per app per month. Roughly 300,000 yen per month in production cost is not a number that pencils out for an indie operation, and it grew faster than ad revenue did. The other option was to slow down, and slowing down on a wallpaper app means the catalog freshness signal that drives DAU starts to bleed.

So the strategy I settled on is "keep one of my own paintings as the anchor, and use generation only to expand the variation surface around it." The world view stays mine. Authorship stays mine. The machine only handles the lateral expansion, where it is genuinely better than I am at scale. Imagen 4 is excellent, but it cannot conjure something like "the texture of prayer" from nothing — that quality has to be present in the seed. Once it is, expansion is a much easier ask. This framing also keeps the relationship with my own practice honest. I am not asking the model to invent the work; I am asking it to extend a piece I already feel responsible for.

Why I split analysis and generation across two models

The early design call I agonized over the longest was whether to feed the source painting straight into Imagen 4 with a prompt, or insert Gemini 3.2 Pro in front as a structural reader.

I went with a clean two-stage split. Gemini 3.2 Pro takes the painting and returns a structured JSON description — palette, composition, negative-space ratio, mood axes, things to forbid. Imagen 4 then takes that description and emits the variations at the right aspect ratio. A second Gemini 3.2 Pro call later acts as the quality judge.

The reason is empirical. When I gave Imagen 4 the painting directly and asked for variations, the surface attributes (color, broad shape) tracked well, but the abstract spine — "prayer," "stillness," "presence" — drifted by the third or fourth image. By the eighth or ninth variation in a series, I would be looking at something that shared a palette with the source painting but had nothing of the original's gravity. When I had Gemini 3.2 Pro decompose the painting first — "diamond-shaped negative space in the center, saturation pulled down by about thirty percent, gaze drawn to the upper right" — the prompts to Imagen 4 stabilized and the series held together across all sixteen variations.

There is a second, less obvious reason for the split. The structured JSON gives me a versionable artifact. If I want to re-generate a series next year against a future Imagen release, I can replay the same palette, composition, and mood axes against the new model and get a faithful refresh without having to rerun the original analysis. The structural read becomes the canonical representation of the painting from the pipeline's point of view. The image generator becomes interchangeable.

This is a fairly natural fit for Function Calling, and the basic patterns from the Function Calling complete guide carried over directly. I did not actually need the tool-use plumbing for this pipeline — the JSON response shape from Gemini 3.2 Pro was enough — but the mental model is the same: separate "what does the model read" from "what does the model write."

Step 1: have Gemini 3.2 Pro read the painting

The first stage takes the source painting and returns a structured JSON description. I am using the google-genai SDK.

# requirements: google-genai>=0.5
from google import genai
from google.genai import types
import base64, json, pathlib
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
SYSTEM_INSTRUCTION = """
You are an art director. Decompose the input painting into a structured
description that a downstream image generator can use to produce coherent
variations. Return JSON matching this schema:
{
  "palette": ["five dominant colors as hex codes"],
  "saturation_bias": "low|mid|high",
  "composition": "centered / rule-of-thirds / negative-space-led / diagonal ...",
  "negative_space_ratio": 0.0-1.0,
  "mood_axes": ["pick 2-3 from: stillness, prayer, awe, elation, ..."],
  "forbidden_elements": ["faces", "logos", "text" — as appropriate]
}
"""
 
def analyze_artwork(image_path: str) -> dict:
    image_bytes = pathlib.Path(image_path).read_bytes()
    response = client.models.generate_content(
        model="gemini-3.2-pro",
        contents=[
            types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
            "Return the structured JSON description.",
        ],
        config=types.GenerateContentConfig(
            system_instruction=SYSTEM_INSTRUCTION,
            response_mime_type="application/json",
            temperature=0.2,
        ),
    )
    return json.loads(response.text)
 
if __name__ == "__main__":
    spec = analyze_artwork("artworks/inori_001.png")
    print(json.dumps(spec, ensure_ascii=False, indent=2))

Temperature is pulled down to 0.2 because I want the same painting to read essentially the same way across runs. If this analysis drifts, every downstream variation drifts with it, and the series stops feeling like a series. I tested at 0.0, 0.2, and 0.5; at 0.0 the model occasionally returned overly literal descriptions ("a dark square in the middle"), at 0.5 the mood axes were colorful but kept reshuffling, and 0.2 hit the sweet spot where the descriptions were both consistent across runs and lyrical enough to give Imagen 4 something to work with.

A real output looks like this (lightly trimmed):

{
  "palette": ["#1A1F2E", "#2E3A50", "#7891A6", "#C8B79A", "#E8DCC0"],
  "saturation_bias": "low",
  "composition": "centered",
  "negative_space_ratio": 0.62,
  "mood_axes": ["stillness", "prayer"],
  "forbidden_elements": ["faces", "logos"]
}

Once the painting is structured this cleanly, Imagen 4 stops freelancing on tone and stays in the world I want. Worth noting: the forbidden_elements field is doing real work here. Without an explicit "no faces, no logos" instruction, Imagen 4 would occasionally interpret negative space as an opportunity to drop in a small figurative element, which is exactly what a wallpaper does not need.

Step 2: translate the structure into an Imagen 4 prompt

The next stage takes that JSON and turns it into a natural-language prompt for Imagen 4. Imagen 4 expects prose, so the palette and composition need to be re-expressed. I let Gemini 3.2 Pro do this rephrasing in early prototypes, and then collapsed it into a deterministic Python function once I had a stable template — there is no learning value in re-running an LLM for what is essentially a string substitution, and the template version is faster and free.

def build_imagen_prompt(spec: dict, variation_seed: int) -> str:
    palette_str = ", ".join(spec["palette"])
    mood_str = " and ".join(spec["mood_axes"])
    forbidden = ", ".join(spec.get("forbidden_elements", []))
 
    prompt = (
        f"Abstract artwork with a {spec['composition']} composition, "
        f"using a restrained color palette of {palette_str}. "
        f"Saturation should be {spec['saturation_bias']}, "
        f"with approximately {int(spec['negative_space_ratio'] * 100)}% negative space. "
        f"Evoke a sense of {mood_str}, suitable for a phone wallpaper. "
        f"Variation hint #{variation_seed}: subtle shift in light direction "
        f"and texture grain, while preserving the overall composition. "
        f"Strictly avoid: {forbidden}."
    )
    return prompt
 
def generate_variation(spec: dict, seed: int, out_path: str):
    prompt = build_imagen_prompt(spec, seed)
    result = client.models.generate_images(
        model="imagen-4.0-generate-001",
        prompt=prompt,
        config=types.GenerateImagesConfig(
            number_of_images=1,
            aspect_ratio="9:16",   # phone portrait
            output_mime_type="image/png",
            safety_filter_level="block_low_and_above",
        ),
    )
    pathlib.Path(out_path).write_bytes(result.generated_images[0].image.image_bytes)

aspect_ratio="9:16" is non-negotiable for wallpapers. I started by generating at 1:1 and cropping in post, and roughly half of each batch lost the compositional center of gravity in the crop. Locking the aspect ratio at generation time fixed that entirely. The variation_seed line in the prompt is a small but useful trick: by saying "subtle shift in light direction and texture grain" with a numeric seed, I get genuine variation across the sixteen images in a series rather than near-duplicates, while keeping the overall composition stable.

The other detail worth flagging is safety_filter_level="block_low_and_above". With abstract art, certain combinations of high-contrast shadow and warm reds tripped the safety filter even though nothing remotely unsafe was happening, and a tenth to a fifth of every series silently failed. The official documentation will not warn you about this; it took two days of generating into the void before I traced it. The pragmatic answer is to handle the empty-result case explicitly, log it, and decide whether to retry with a slightly tamed palette or move on.

def generate_variation_safe(spec: dict, seed: int, out_path: str) -> bool:
    prompt = build_imagen_prompt(spec, seed)
    result = client.models.generate_images(
        model="imagen-4.0-generate-001",
        prompt=prompt,
        config=types.GenerateImagesConfig(
            number_of_images=1,
            aspect_ratio="9:16",
            output_mime_type="image/png",
            safety_filter_level="block_low_and_above",
        ),
    )
    if not result.generated_images:
        # Logged so I can audit the prompt later. Returning False means the
        # caller decides whether to retry with a different seed or skip.
        print(f"[SAFETY-FILTER] dropped seed={seed}: prompt={prompt[:140]}")
        return False
    pathlib.Path(out_path).write_bytes(result.generated_images[0].image.image_bytes)
    return True

Step 3: put a quality gate on every fourth file, not the last

If you ship straight from Imagen 4, a small fraction of the outputs will look subtly wrong — a misplaced glyph in the negative space, a composition that breaks the eye line, a color drift toward the warm end that throws off the series. The model is good, but in 120 deliveries you will hit five to eight that are obvious rejects on first glance and another fifteen that you only catch when you put them next to the other variations.

So I bring Gemini 3.2 Pro back in as a judge. It looks at the original painting and the generated variation side by side, and scores four axes.

QC_INSTRUCTION = """
Compare two images. The first is the source painting; the second is a candidate variation.
Score each axis from 0.0 to 1.0 and return JSON.
{
  "palette_match": how closely the colors align,
  "composition_consistency": how well the composition is inherited,
  "mood_consistency": how well the mood is preserved,
  "wallpaper_safety": absence of text, faces, logos
}
Treat all-axes >= 0.7 as accept, any axis < 0.5 as reject, and anything in between
as pending human review.
"""
 
def qc_check(original: str, candidate: str) -> dict:
    parts = [
        types.Part.from_bytes(data=pathlib.Path(original).read_bytes(),  mime_type="image/png"),
        types.Part.from_bytes(data=pathlib.Path(candidate).read_bytes(), mime_type="image/png"),
        "Follow the instructions and return JSON.",
    ]
    response = client.models.generate_content(
        model="gemini-3.2-pro",
        contents=parts,
        config=types.GenerateContentConfig(
            system_instruction=QC_INSTRUCTION,
            response_mime_type="application/json",
            temperature=0.1,
        ),
    )
    return json.loads(response.text)

The placement decision was important. My first instinct was to gate at the end of a series, after all sixteen variations were out — but by then I had already paid for the rejects, both in API cost and in turn-around time. Putting the gate after every fourth generation means I can catch a series that is drifting early, adjust the prompt, and not waste the remaining twelve calls on a path that was not going to converge.

Inserting this gate at every fourth generation changed the math. In week one, with no gate, I had to generate 247 candidates to ship 120 (49% acceptance). After the gate went in, the same 120 took 168 generations (71% acceptance), and the API bill dropped by about a third. The gate itself adds 30 Gemini 3.2 Pro calls per series, which is a rounding error against the savings on Imagen 4 generations.

Step 4: pack for the app

Once a wallpaper passes QC, I export it for both iOS and Android at three target resolutions: 1290×2796, 1170×2532, and 1080×2340. Imagen 4 returns 1024 or 2048 squares, so I post-process with Pillow to fit and pad.

from PIL import Image
 
TARGET_SIZES = [(1290, 2796), (1170, 2532), (1080, 2340)]
 
def export_for_app(src: str, slug: str, out_dir: str, palette: list[str]):
    img = Image.open(src).convert("RGB")
    # Pad with the first color of the source palette, not flat black.
    pad_hex = palette[0].lstrip("#")
    pad_rgb = tuple(int(pad_hex[i:i+2], 16) for i in (0, 2, 4))
 
    for w, h in TARGET_SIZES:
        canvas = Image.new("RGB", (w, h), color=pad_rgb)
        ratio = min(w / img.width, h / img.height)
        new_w, new_h = int(img.width * ratio), int(img.height * ratio)
        resized = img.resize((new_w, new_h), Image.LANCZOS)
        canvas.paste(resized, ((w - new_w) // 2, (h - new_h) // 2))
        canvas.save(f"{out_dir}/{slug}_{w}x{h}.jpg", quality=92, optimize=True)

The small touch that mattered is filling the padding with the first color from the source palette, not flat black. Once an icon grid sits on top of the wallpaper, the visual continuity is much stronger, and the store screenshots feel cohesive. JPEG at quality 92 with optimize=True is roughly a third of the size of an equivalent PNG and visually indistinguishable on the device — useful when you ship hundreds of variants per app and bundle size matters.

How the orchestration glue actually looks

It is worth seeing the pieces sit together. The driver below runs one source painting through the full pipeline: analysis, four-by-four generation with a quality gate every fourth file, and final export. I keep this as a thin script rather than a framework, because at the volume I am running — twelve series a month — the operational overhead of a full orchestration tool would cost me more time than it saves.

import json, pathlib
 
def run_series(source_painting: str, series_name: str, target_count: int = 16):
    src = pathlib.Path(source_painting)
    out = pathlib.Path(f"out/{series_name}")
    out.mkdir(parents=True, exist_ok=True)
 
    # 1. structural read
    spec = analyze_artwork(str(src))
    (out / "spec.json").write_text(json.dumps(spec, ensure_ascii=False, indent=2))
 
    accepted = []
    seed = 0
    while len(accepted) < target_count and seed < target_count * 2:
        seed += 1
        candidate = out / f"cand_{seed:03d}.png"
        if not generate_variation_safe(spec, seed, str(candidate)):
            continue
 
        # quality gate every fourth candidate
        if seed % 4 == 0:
            score = qc_check(str(src), str(candidate))
            if all(v >= 0.7 for v in score.values()):
                accepted.append(candidate)
            elif any(v < 0.5 for v in score.values()):
                candidate.unlink(missing_ok=True)
            else:
                # middle zone: keep, but flag for human review
                (out / "needs_review.txt").write_text(f"{candidate.name}\n", append=True) \
                    if hasattr(pathlib.Path, "write_text_append") else None
                accepted.append(candidate)
        else:
            accepted.append(candidate)
 
    # 2. export the first target_count accepted
    for i, cand in enumerate(accepted[:target_count], start=1):
        export_for_app(str(cand), f"{series_name}_{i:02d}", str(out / "export"), spec["palette"])
 
    return {"series": series_name, "generated": seed, "shipped": min(len(accepted), target_count)}

The only piece of operational discipline I added on top of this is logging the per-series numbers — generated count, shipped count, and total token + image cost — to a small SQLite table. After thirty days of these rows accumulating, it became obvious which paintings were "easy" for the pipeline (palette contained, clear composition) and which were hard (very wide tonal range, ambiguous subject). I started picking source paintings with the easy attributes for the first batch each week, which raised the throughput meaningfully.

The middle-zone problem and why I stopped trying to automate around it

The QC gate divides candidates into three buckets: accept, reject, and middle zone. The accept and reject buckets are easy. The middle zone is where most of the operational effort actually goes.

A middle-zone image has all four QC scores above 0.7, but a human glance lands on "something is slightly off." The drift might be that the negative space has a faint horizontal banding that was not in the source, or that the warm tone in the palette has shifted by half a hue toward orange, or that the texture grain reads more synthetic than painterly. None of these are the kind of thing a numeric score easily captures.

I spent a week trying to push the QC instructions into catching this. I added an "aesthetic_coherence" axis. I tried prompting the judge to compare against the previous accepted variation in the series, not just the source painting. I tried asking for a free-text "concerns" field alongside the score. None of it moved the false-pass rate enough to retire the human eyeball.

What I landed on is much simpler. After the QC gate, a human (in this case, me, while drinking coffee in the morning) reviews the middle-zone candidates at five seconds per image. A series of sixteen has on average two middle-zone images, so the daily review time across twelve series in a month works out to about two minutes total. That is small enough that automating it harder would have been a worse use of my attention.

The lesson I take from this: the right automation boundary is not where the model could maybe handle the task with more prompt engineering. It is where the cost of the remaining human work is small enough that you stop thinking about it. Past that boundary you are buying engineering time that does not pay back.

What thirty days of running this actually cost

Twelve source paintings, twelve series, 168 generations, 120 ships. Here are the real numbers.

Gemini 3.2 Pro spent across structural analysis and quality checks: 408 calls, roughly 1,720 yen (about US$11). Imagen 4 Standard generations: 168 calls, roughly 6,720 yen (about US$45). Total: 8,440 yen, around US$56, working out to about 70 yen per shipped wallpaper.

The previous outsourced cost was 8,000 to 15,000 yen per wallpaper. The shipped pipeline costs less than 1% of that on a per-piece basis. I do not love using cost compression as a headline number, because the pipeline is taking on labor that real human collaborators used to do — but it does mean an indie operation can keep refilling the catalog without taking on contract debt, and that has real value when you are running a portfolio at this scale on your own. The collaborators I used to work with have moved into other parts of my practice that the pipeline cannot do, which has been a healthier outcome than I expected.

The downstream KPIs were the result I actually cared about. Across the seven days following each new series shipping, DAU on the receiving apps came in 14.3% above the trailing twelve-week average for the same category, and average session time was up 8.7%. AdMob eCPM did not move (which I would not expect to in this short window), so the lift in session time translated almost one-for-one into ad revenue lift. Across a portfolio that had started to plateau after fifty million cumulative downloads, this is a meaningful nudge — not a transformation, but enough to take the catalog from "managed decline" back to "slow, steady growth."

The honest losses are worth recording too. Twice in thirty days the safety filter rejected an entire series because the composition drifted close to a "figurative human form" reading. I added an explicit avoidance line in the prompt, but the first one cost me half a day before I figured out what had happened. Separately, about ten percent of the QC scores landed in the "all axes above 0.7 but it just looks slightly off" middle zone. For now, a five-second human eyeball on each finalist is the right answer; trying to push the model into making this judgment for me has not paid off, and frankly I am not sure it should.

A short defense of starting from your own work

Before closing, I want to address the obvious counter-question: why use my own paintings as the seed at all? An indie developer running a wallpaper app could prompt Imagen 4 directly from scratch, generate hundreds of designs at zero seed-acquisition cost, and ship them without ever touching a brush.

Three reasons. First, copyright clarity. When the seed is unambiguously my own work, the derivative wallpapers have a clear authorship line, which matters for store dispute resolution and for any future commercial use of the material. Second, world view consistency. The painting carries a coherent set of choices that the model would never converge on by itself, no matter how careful the prompt. Variations from a strong seed are noticeably less generic than designs generated cold. Third, sustainability of practice. The pipeline keeps me painting. If the system makes my hand-work more useful at the scale my apps actually need, I keep producing original work; if I let the model take over the seed step too, the practice slowly hollows out, and within a year the apps would all converge on the same generative aesthetic the rest of the App Store is converging on.

This third point is the one I think about the most. The pipeline only works as long as there is a real seed. If the seeds dry up — because I stop painting, or because the practice that produces the paintings stops being nourished — the entire system is just rearranging machine-generated patterns in a closed loop, and the audience picks up on that drift faster than the metrics do. So as much as this is an engineering article, it is also an argument for keeping the human practice intact upstream of the pipeline. The seed is what the model cannot replace, and protecting the conditions that produce good seeds is part of the job description.

If you want to try this yourself

Pick three source pieces of your own. Run them through structural analysis, generate four variations each, gate them, and export. The total cost should sit between 200 and 300 yen, well under three dollars. After one full series end-to-end, you will have a much sharper feel for which parts of your work the machine can extend without losing the spine, and which parts only your hand will ever produce. You will also have a baseline for the QC scores in your own visual register, which matters more than you would think — my "stillness" is not the same as another artist's "stillness," and the score thresholds need to be calibrated by feel before you trust them in production.

I started learning programming on my own in 1997, when I was sixteen and the internet first let me reach designers and engineers across borders. Both of my grandfathers were temple carpenters, and I grew up around the idea that working with your hands is itself a quiet form of devotion. I bring the same instinct into this kind of pipeline work — the seed has to come from a hand that took responsibility for it, and only after that does it make sense to let the system do the lateral expansion. Generative AI does not retire the artist; it gives the artist a much larger surface to keep their world view alive on. I hope this log is useful if you are walking the same line.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Advanced2026-03-31
Build a Personal AI Secretary with Gemini API — Task Automation, Email Summaries & Schedule Optimization for Solopreneurs
A complete guide to building a production-grade AI secretary system for freelancers and solopreneurs using Gemini API. Covers Function Calling implementation for task automation, email summarization, and schedule optimization, all the way through Cloud Run deployment.
Advanced2026-07-10
My ADK Assistant Quietly Forgot a Deadline — Catching Compaction Memory Loss With a Recall Probe
Compacting conversation history in Google ADK with Gemini lowers cost, but it also erodes what your assistant remembers — silently. Here is how I built a recall probe to measure that loss, compared three compaction strategies against the same ledger, and stopped trading memory for tokens.
Advanced2026-07-09
Setting a Token Budget Per Free User: Balancing AdMob Revenue Against AI Feature Cost
Rate limits protect requests per minute. They do nothing for the invoice that arrives at the end of the month. Here is how I derive a per-user token budget from ad revenue, keep the ledger inside a single call wrapper, degrade gracefully at a soft cap, and detect abuse with one concentration ratio.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →