← Back to painting canvas
Engineering Retrospective

The Neverending Painting — The New Dev World

A vibe coder's guide to shipping a not-so-small, production-ready project.

By Ran Shaltiel20+ Years in Dev / Solo BuilderSeptember 2026
TL;DR (a better summary than any LLM!)
  • I started a project I thought would be done fast with AI ("3–4 prompts with a feedback loop and it's done").
  • Reality snapped back, but I decided to see it through, to learn what it actually takes to vibe-code a full, production-ready, not-so-small project.
  • I got trapped in vibe-coding hell, and engineered my way out with mostly Gemini 3.8 (Fable was still needed from time to time), a lot of stubbornness, and even some architectural input of my own.
  • I worked out how to attack the next project: what to expect, how to start, and what to avoid.
  • Jump straight to the concrete advice ↓
  • Try the live result: neverendingpainting.art

Origin Story

So I had this idea. I thought it was pretty neat, and the best part? With today's AI it shouldn't take long. Too many projects are already dying in the proj drawer on my laptop. I knew this time had to be different. This one had to get finished.

After all those dead projects, I had two hard requirements for the next one:

  • It has to be something I actually like.
  • It cannot be too ambitious.

If that sounds trivial to you, good for you. I have wasted a lot of time building things I thought were needed, even when they were really boring to work on. The logic was always the same: AI can do it, someone needs it, so let's build it. That logic falls apart the moment the AI gets stuck and needs my attention after a long day at my actual job.
And when I did find something that excited me, it was too big. Even for today's most sophisticated models.

So this time, all of that was going to change. I had something I liked. The idea excited me. It needed serious tech, and it sounded like the top models could solve it in a few days. YAY!

The Core Concept

Simple. You start with a painting. You explore it by panning and zooming, and when you reach a place nobody has ever visited, a generative image model is called to paint it. Bit by bit the world grows into a huge canvas, and when you reach the edge of it, you just keep going.

Because generative AI takes inference time and costs real money, it had to be a shared, collaborative world. The first explorers don't see much, since almost nothing has been discovered yet, but they get to claim the top-level places, the ones everybody passes through. The deeper you zoom, the fewer people will ever see that spot, so the early land is the prime real estate: you name it, and your name stays on it. Late arrivals get the opposite deal. They walk into a huge world that is already full of other people's discoveries.

And so it was decided: a shared, collaborative, neverending painting. That's it. A weird project with no conventional "utility". It's not a SaaS tool, it's not a productivity app, it's not educational, and it's not quite a game. It is simply a shared map that expands forever into the unknown. For some reason, that excited me.

I'm not a novice. I've been in the dev business for more than 20 years, so I know that having the idea is the easy part and that finishing is the hard part. But this one seemed doable. Sure, I would need the expensive model to solve some of the border and concurrency problems, but doable. And even if no real users ever showed up, I'd at least have something fun to show my kids. Small downside. Yep, let's go.

The Naive Architecture

A quick search confirmed nobody had built this specific multi-resolution collaborative canvas. I drafted a name and the core product mechanics:

  • Google Earth style deep-zoom navigation: pan, zoom, keep zooming.
  • Explorers enter the world, see what others have already discovered, and when they reach the edge of the known world, they can discover new land.
  • Those newly generated pixels belong to the discoverer. They can name the place, share a permanent link to it, and feel a healthy sense of pride and glory.
  • The catch: AI image generation costs real money. I'm a solo dev with three little boys and a hefty mortgage; I can't sponsor unlimited generations for the entire internet. So every new explorer gets 3 free discoveries ("Pigments"). If someone enjoys exploring, they can buy a handful of extra pigments, which is essentially buying me a coffee in exchange for more canvas.

Pleased with myself, I rolled up my sleeves and started talking to Fable and Sol. With today's AI, I can probably ship it after 3–4 prompts.

Stack-wise I went fully vibe-coded and let the model pick its own tools. I only asked it not to use Node (sorry, not sorry). It landed on React and TypeScript in the browser, Python and FastAPI on the server, and OpenSeadragon for the pan and zoom tiling.

📋 The original ROADMAP.md (back when this was a weekend sprint)
  1. Pay Google / integrate the image generation API
  2. Make sure the core zoom loop works on the canvas
  3. Sign in / register with Google OAuth
  4. Discoverer profile, balance, list of discovered places
  5. User payments
  6. Deploy to Vercel
  7. Fix the concurrency and neighbouring-image bugs
  8. Polish (and a logo, of course!)
  9. Security checks
  10. Friends & family beta test
  11. Launch & monitor
Reality Check #1

The First Zoom & The Moon Seam

Tokens flew through the air, files materialized across the workspace, and that intoxicating late-night LLM magic was in full effect. A working prototype appeared on localhost.

I loaded the seed image, an abstract vector landscape with dark mountains and a golden moon, moved the canvas around, zoomed in on the moon and clicked discover. A spinner appeared. And kept spinning for a couple of minutes. (Most of that, I found out much later, was my dev machine patiently waiting on a network route that did not exist.)

While waiting, I naturally wandered off across the canvas to see other parts of the world. When the generation finally finished, I was completely lost in a different quadrant. I spent two minutes just panning around, trying to find the square of land I had generated.

Found it. The new tile had cut a flat, 90-degree corner straight through the round moon, with a halo that matched nothing around it:

Original Seed Painting
The original seed painting (seed.png), where the universe started. 🔍
First Zoom Seam Bug
The real zoom output (first-moon.webp), a harsh 90° seam cut through the moon. 🔍

I did expect this class of bug, and I even warned the model about it up front, but it turned out to be much harder to fix than just telling Fable to fix it.

It became a loop: test, prompt, test, prompt. And every time it looked fixed, something else broke and earned another prompt. I tried telling the model to run it and check for itself, but it could not tell a good result from a bad one without me. It can see the image perfectly well. It just doesn't know what "wrong" looks like here.

Reality Check #2

Seams, Outpainting & The Hybrid Pipeline

Discovering a sector next to an existing one looked dreadful. In our diagnostic log (SEAM_CONTINUITY.md) we, and by "we" I mean the AI agents alone, measured the colour disagreement right at the boundary: a peak step of 75.7 per pixel, with the drift growing from 13.7 to 28.4 across the band next to the join. In plain words: a visible scar running down the middle of the painting. No amount of prompt engineering on a single model resolved it.

Eventually the AI suggested bringing in FLUX.1 Fill, which is good at a completely different job. Instead of inventing a picture from scratch, you hand it the existing image with a hole punched in it, and it paints only the hole, matching the edges it can already see.

The Hybrid Generation Routing Pipeline
When an explorer reveals new land, the pipeline checks whether it touches already-discovered neighbours. Frontier land goes to the FLUX.1 Fill pipeline, which matches the existing border. Virgin land goes to the Gemini Nano pipeline, which generates freely.Explorer Initiates RevealHas DiscoveredNeighbors / Border?YES (Frontier)FLUX.1 Fill Pipeline1. Gemini captions parent crop2. Padded boundary context3. Gaussian tone lock (σ = 2.0)NO (Virgin Land)Gemini Nano Pipeline1. Fast creative generation2. Full-buffer synthesis3. Baseline canvas anchor
← swipe to see the full diagram →
Figure 1: Hybrid routing decision tree. Virgin land gets creative Gemini synthesis; frontier land touching existing pixels gets conditioned FLUX.1 inpainting with Gaussian boundary tone locks.
Reality Check #3

Fable & Sol Out, Gemini In

By this point it was obvious this was not going to be a weekend project. Every fix needed more prompts, and every prompt needed more fixes.
It was a decision point. This is a genuinely time-consuming project, and time is the one thing I don't really have. But it is fun, and I would get to learn exactly where the latest tech shines and where it falls apart. So this time I'm going all the way.

Unfortunately, I don't have the budget to keep running Fable and Sol at that pace, so I needed a new plan. Gemini 3.7 Flash had just been released, and I decided to give it a try.

It wasn't bad. I could work with it. I still needed Fable for the hard planning and for reviewing big changes, but Flash was blazing fast and it did nicely. Soon after, 3.8 Flash was released, and wow, it was (and still is) actually good. In Antigravity you get fast mode for free, as of today: really fast, really cheap. I ran loops of agents in parallel and still had tokens to spare. It is still not Fable and it is not Sol, but I found myself reaching for them less and less, because Gemini got the job done.

Reality Check #4

The State Machine & The Lost Explorer

Fixing the art revealed the distributed-systems nightmares lurking underneath:

  • The concurrency collision: what happens when two explorers try to discover the exact same frontier in the exact same second?
  • The freezing camera: if generation takes 15–25 seconds, locking the explorer's camera behind a modal spinner (my original fix for people getting lost) is agonizing. But if you let them roam freely, where does their new land go when it finally arrives?
  • The orphaned token: if an explorer closes the browser tab mid-generation, do they lose their token? Does the server throw away pixels that just cost real API dollars?

I was really pleased with myself for designing the formal Territory & Ceremony state machine that solved this. (This time it was actually me being useful, finally.) The trick was to cleanly separate the machine's job, making pixels, from the human's job, owning them:

Territory State Machine Lifecycle
Land moves from UNKNOWN to PROCESSING when an explorer reveals it, then to MATERIALIZED once the pixels are published but still fogged and unowned, and finally to DISCOVERED when the explorer's browser acknowledges the render and ownership is assigned. A timeout or error sends it back to UNKNOWN and refunds the token.UNKNOWNInitial Uncharted VoidRevealPROCESSINGWorker Lease Lock (≤ 4m)PublishMATERIALIZEDFogged & UnclaimedRender AckDISCOVERED✨ Lit Pixels & ContoursOwnership Assigned · Escrow PaidTimeout / ErrorFAILED / TIMEOUTTokens Refunded · Leases ClearedReverts to Void📡 Live WebSocket Shimmer Box
← swipe to see the full diagram →
Figure 2: The formal Territory State Machine. Pixels are generated and committed as fogged MATERIALIZED chunks before human ownership is committed upon client render-acknowledgment.

Because pixels land as MATERIALIZED and fogged before anyone claims them, an explorer who disconnects never wastes the money I already paid. The land stays in the world, ready for the next person who wanders in to claim it with zero wait.

And the explorer who wandered off across the painting while their generation was running? We built the Discovery Toast: a floating notification that pops up saying "✨ New Horizon Discovered! [Jump to Location]". One click glides the camera smoothly across the world, straight to their new landmark.

Reality Check #5

The 30-Bug Markdown Strategy

Bugfix Folder
Figure 3: the actual docs/bugfix/ folder (30+ logs). Click to expand 🔍

Pretty soon I ran into regression hell. (I did crawl back to Fable. It didn't really help.) You ask the AI to fix a border glitch and it quietly breaks the deep-zoom level calculation. You fix the zoom, and the live presence of other explorers drops off.

Instead of letting the AI write a separate, isolated markdown document for every bug, I instituted a rule: related bugs (85%+ the same domain) must share the same living document.

Take a look at the screenshot: SEAM_CONTINUITY.md (49 KB), LANDMARK_NAVIGATION.md (91 KB), IN_PROGRESS_PROCESSING_ZONE.md (58 KB). Each file logs every symptom, every attempted patch, why the previous attempts failed, and the verified resolution.

When a new bug came up, I didn't say "fix this". I told the model: "Read docs/bugfix/SEAM_CONTINUITY.md first, understand what was already tried and why it broke, and do not repeat those mistakes." That single discipline ended the regression loops.

Next project, I'll slice it into its main features from the beginning, and give each slice its own markdown file for the AI to read when it needs it.

Reality Check #6

Infrastructure & The Payment Gauntlet

Apparently Vercel was not the right approach for me. Serverless functions cap how long a single request may run (10–60 seconds), keep memory tight, charge real money for bandwidth, and have no good answer at all for a long image pipeline or for the always-open connections that let explorers see each other move.

I moved to a Netcup VPS running Docker Compose (Caddy, four FastAPI workers, Redis 7, PostgreSQL 16, MinIO). Rock solid, blazing fast, and it costs a tiny fraction of the serverless bill.

I must say the AI did pretty well with the deployment itself. But I still had to work out what was best, compare prices, register, and run it. AI helps with all of that. It just doesn't take the time off your calendar.

The Payment Odyssey

I asked Gemini, and it said Stripe. It wrote everything that was needed while I did the paperwork. Apparently Stripe doesn't support my country without an expensive US entity setup.

  • Next suggestion, Paddle: they support my country! I swapped the code, registered, and got a rejection:
    "After evaluating the details provided, we have determined that we cannot move forward. This decision is final." That was strange, so I tried to understand why from a human in sales: "As a Merchant of Record... our Acceptable Use Policy prohibits Generative AI."
  • Dodo Payments: terms prohibited generative AI.
  • PayPro Global, Lemon Squeezy, Creem: each one meant another round of registration and another evening reading the fine print. All of them either rejected me or turned out to be incompatible with virtual currency, generative AI, or both.
  • Xsolla: finally! My country, the game mechanics, the virtual currency ("Pigments") and the generative AI were all acceptable to them. But why should it be easy? They had questions about the project (which probably did look suspicious from the outside), asked for my physical address, reviewed the site, and every email took days.

Weeks of calendar time. Almost no code.

Adding Personality & Human slop

When you discover a place you unlock perks and titles. That is eventually what makes the game feel like a game. I tried to pour my own personality into that text and, yet again, those things take time. I must admit I ended up asking Gemini for help with some of it. Without that, the project would have slipped another month.

Creating this file, only this small info file takes more than day or two, if you want to write it old fashion way

The Logo, The Loading GIF & "Found a World"

From the very beginning I knew exactly how I wanted the logo to look: vintage, low detail, Fallout 3 style, with a magnifying glass. But no matter how hard I tried, or which model I used, I couldn't get it right. In the end I found something I like, even though it isn't what I originally had in mind. I also created a custom animated loader (loading.webp) for tiles that are still rendering. It looks great at full resolution. I know you can barely make it out at icon size, but even then it holds a nice shape:

Vintage Emblem Logo
The official crest (4icon.jpg) 🔍
Animated Loading GIF
The custom animated loader (loading.webp) 🔍

Along the way, two new features came to life surprisingly easily:

  1. Discoverer prompts: explorers can type their own text before uncovering a tile ("In this specific location, the explorer discovers: ..."), directing the AI's imagination.
  2. "Found a World": explorers can upload their own seed image and spawn an entirely new infinite painting for the community to explore. That whole feature took only a few focused prompts.

Where AI Excels: The Math Under the Hood

Even this took more than one or two prompts, because the hierarchical coordinate math is fiddly. But this is the part where the model genuinely shines, so for this section I'll just let it show off. (Not a programmer? Skip straight to the epilogue, this one is the engine room.)

  • Multi-resolution coordinate space: levels z in [0, 5], where level z is 1024 × 2z pixels on a side, scaling from 1024² at seed level 0 to 32,768 × 32,768 at depth 5.
  • Viewport level mapping formula:
    level = MAX_LEVEL - ceil(log2(longest_dimension / 1024) - 1e-9)
    Using ceil with an epsilon absorbed the floating-point noise and eliminated the center-crop border stripes.
  • 8 KB chunk bitmasks: each 256 × 256 chunk tracks discovery via packed bitmasks (65,536 bits in row-major order, bit-packed with NumPy). Discovery credit is measured down to the individual pixel.
  • Doubled-coordinate URL hashes: OpenSeadragon sub-pixel floats caused subtle rounding drift when a link was shared across devices. We adopted integer half-pixel doubled coordinates (cx2 = round(cx * 2)) in the URL hash (#v=2&cx2=...&cy2=...), so every shared link lands on exactly the same view.

Epilogue: Why Does It Still Take So Long With All This AI Of Yours?

Lots of bugs Point 7 at roadmap was a single line, "fix the concurrency and neighbouring-image bugs", sitting between OAuth and the logo. That one line became the project. docs/bugfix/ holds more than thirty post-mortems, and three of them alone come to nearly 200 KB of symptoms, failed patches and retries. The first working version of almost every feature arrived in an evening. Everything after that was me finding out what was wrong with it.

And the bugs were slow because the model cannot tell a fix from a break. It painted a square corner through the moon and had no idea anything was wrong. It could produce a candidate fix in seconds, but not tell me whether the fix worked, so every single round trip came back through my eyes. Machine-speed generation, human-speed verification. That gap is the whole bottleneck, and no bigger model closes it.

The bugs that mattered were not prompt problems at all. The seam took a second model doing a different job. The lost explorer took a state machine separating making pixels from owning them. You do not prompt your way to either of those, you decide them.

Memory and understanding helped: Fix the border, break the zoom. Fix the zoom, lose the presence. That loop only ended once the bug logs existed and the model was told to read them before touching anything. Most of my "prompt engineering" turned out to be writing things down.

And a stubborn tail was never code at all. Comparing hosts, seven payment providers, rejection letters, acceptable use policies, emails that take days, writing the lore in my own voice. AI helped with every one of those. It shortened none of them to zero.

So yes code is (almost) solved. It was just never the whole job.

Small Note For the Not-So-Technical Builders

AI gave the power to build and skip years of learning, but building an end to end project still require dedication and lots of hard work.

  • Do not be afraid about technical stuff you do not understand from this post, just keep asking AI about how and why. Just command it to do, still does not enough.
  • Think how to orginize his memory, telling him "we already talked about it" is almost useless. At least today.

Start exploring · 3 free discoveries →

Frequently Asked Questions

What is The Neverending Painting?

The Neverending Painting is a free, collaborative digital artwork with unbounded zoom depth. When an explorer zooms into unexplored territory, generative AI models paint what lies deeper, seamlessly and permanently expanding the shared canvas for everyone.

Is it free to explore the painting?

Yes, exploring and zooming across the painting is completely free and requires no account. An account and Pigments are only needed when you choose to trigger new AI-painted deep generations or name newly discovered places.

How deep can you zoom?

The painting has unlimited zoom depth. The deep-zoom pyramid allows explorers to dive deeper and deeper without a bottom. Each zoom level unveils entirely new, high-resolution micro-worlds painted in seamless continuity.

Who owns a discovered place?

When you discover and name an uncharted location, your explorer name is permanently recorded in the painting's public discovery ledger, and you receive a persistent shareable URL (/place/your-place). All future visitors can see your named discovery and dive deeper within it.

Which AI models power the generation pipeline?

The painting uses a hybrid generation pipeline: Google Gemini image models paint open, unexplored territory (virgin buffers), while Black Forest Labs FLUX.1 Fill handles connected frontier inpainting conditioned on existing adjacent tiles.

How does the painting avoid visible seams between zoom levels?

We use an aligned chunk grid with coordinate snapping, parent-context conditioning with FLUX.1 Fill along connected frontiers, and border alpha-feathering to significantly reduce visible transitions between adjacent tiles and zoom levels.

Is the content safe for all ages?

Yes. All prompt suggestions and generated imagery pass through multi-stage automated safety and moderation filters before any tile is committed to the shared canvas, keeping the world family-friendly.

Concrete Advice (For Myself & Other Vibe Coders)

1
Spend more than a day on each feature before you build it.
It will not take a day or two anyway. The fastest and cheapest way forward is to sleep on the core idea, the product design and the architecture. Every night you give an idea is a night it gets better.
2
Slice your documentation by bounded domain.
Keep 2–3 concise, living markdown files per epic. When you prompt, tell the AI exactly which slice to read, so it doesn't drown in unrelated context.
3
Keep related bugs in the same living document.
One running bug log that teaches the AI what was already tried, and why it failed, is what ends the endless regression loops.
4
The strongest model shouldn't be your main driver.
It is usually much slower, much more expensive, and often plain overkill. A fast model can do most of the work; save the big one for hard planning and for reviewing big changes.
5
Code is mostly solved. The rest is not, yet.
Misunderstood prompts, payment compliance, bugs, unknown unknowns, and all the fine tuning that only surfaces once the product is real: every one of those still lands on your desk, and every one of them takes time.

Cheers,
Ran