How I Built a Local, Multimodal Gemma 4 Visual Regression & Patch Agent: Closed-Loop Validation, Canvas Pixel Diffing, and Reproducible Benchmarks

Patch interface

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

Google’s Gemma 4 brings a paradigm shift to the open model ecosystem: native multimodal capabilities, massive context windows, and dense model architectures tailored for different developer tasks. In this guide, I’ll walk through building a next-generation Visual Regression & Patch Agent using Gemma 4, explain how we implemented closed-loop code safety, share a client-side visual diff verification engine, and present a rigorous 10-case benchmark suite demonstrating 100% success.

🔍 The Problem: The Visual-Code Disconnect

Developers face a frustrating workflow when debugging front-end visual bugs. They see layout overflows, responsive breaks, z-index overlays, or flexbox alignment bugs in the browser, but must manually trace these visual defects back to specific CSS selectors, DOM nodes, or JS component logic.

Conventional AI coding assistants are blind to visual screenshots. While they understand source code, they cannot read a screenshot of a broken page and know why the layout broke. Screenshot regression tools can spot visual differences but are incapable of producing the code patches required to fix them.

Demo

Live URL: https://multimodal-visual-regression-patch-agent.vercel.app

Video Demo: https://youtu.be/gvarF7T1C5E

See the Gemma 4 Visual Regression & Patch Agent in action, illustrating drag-and-drop file ingestion, screenshot visual overlays, patch generation, and real-time validation badges:

.

The Solution: Closed-Loop Visual Repair Agent

The Gemma 4 Visual Patch Agent bridges this gap by combining multimodal vision reasoning with closed-loop patch validation and interactive visual verification. By analyzing a screenshot of a visual bug alongside the corresponding source files, the agent localizes the defect’s exact root cause, writes a clean git-diff patch, validates it for syntactic correctness and applicability, and simulates the visual fix in an interactive before/after split slider and pixel-level heatmap.

Patch interface

Visual display of the interactive Regression Loop application interface

Mermaid Flow

🧠 Why Gemma 4 for Agentic UI Repair?

  1. Native Multimodality: Traditional AI pipelines feed screenshots to a separate vision-encoder model and pass text descriptions to an LLM. Gemma 4’s native multimodal architecture processes text and pixel tokens in a single cohesive space, ensuring high spatial precision.
  2. Extended Context Window: Ingesting raw code modules, stylesheets, and dense base64 image maps is incredibly token-expensive. Gemma 4 handles these easily.
  3. Structured Git Patching: The model generates standard, clean unified git diff patches (--- a/ and +++ b/) that can be validated programmatically.
  4. Open accessibility via free APIs (OpenRouter, Hugging Face) and local deployment options.

Model Selection: Which Gemma 4 Variant to Use?

Gemma 4 comes in three architectures. Here’s how to choose:

Gemma 4 31B Dense (Recommended)

  • Best for: High-quality output, complex reasoning, long-context tasks.
  • Use when: Accuracy matters more than speed or resource constraints.
  • Deployment: Server-grade hardware or cloud APIs.
  • Why I chose it: For code review, precision is critical. A missed bug or incorrect suggestion introduces new problems. The dense 31B model provides the most accurate analysis.

Gemma 4 26B Mixture-of-Experts (MoE)

  • Best for: High-throughput applications with good quality.
  • Use when: You need to process many requests quickly without sacrificing too much quality.
  • Deployment: Server-grade hardware, optimized for throughput.
  • Tradeoff: Slightly lower quality than 31B Dense, but faster inference.

Gemma 4 2B/4B (Small Models)

  • Best for: Edge deployment, mobile devices, browsers.
  • Use when: Resource constraints are primary concern.
  • Deployment: Can run on Raspberry Pi 5, high-end phones, or in-browser.
  • Tradeoff: Limited reasoning capabilities, smaller context window.

Decision framework for your project:

If quality is priority → 31B Dense
If throughput is priority → 26B MoE
If deployment constraints → 2B/4B (Edge)

Getting Started: Free Access Options

You don’t need expensive infrastructure to start with Gemma 4. Here are three free options:

Option 1: OpenRouter (Recommended for Prototyping)

OpenRouter provides free tier access to Gemma 4 31B with no credit card required.

# Get API key from https://openrouter.ai/keys
export OPENROUTER_API_KEY="your-key-here"
export MODEL_CHOICE="gemma-4-31b"

Option 2: Hugging Face Inference API

Free access to Gemma 4 models via Hugging Face’s serverless inference.

# Get token from https://huggingface.co/settings/tokens
export HUGGINGFACE_API_KEY="your-token-here"
export HUGGINGFACE_MODEL="google/gemma-4-31b-it"

Option 3: Local Deployment (Advanced)

Download models directly from Hugging Face or Kaggle and run locally. The 2B/4B models can run on consumer hardware; 31B requires significant RAM (~60GB for full precision, ~30GB with quantization).

# Using Hugging Face transformers
pip install transformers accelerate
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-31b-it",
    device_map="auto",
    load_in_4bit=True  # Quantization for memory efficiency
)

Building Closed-Loop Patch Validation

Never trust AI-generated code blindly. To make the agent production-ready, we built a multi-tiered validation pipeline in backend/patch_utils.py that verifies the safety and syntactic validity of generated patches before returning them to the user:

1. In-Memory Git Apply Check (PatchApplicabilityChecker)

We initialize an ephemeral git repository in a temp directory, write the original source files, and run git apply --check patch.diff. This ensures the patch applies cleanly with zero hunk conflicts.

class PatchApplicabilityChecker:
    @staticmethod
    def check_applicability(patch: str, file_context: Dict[str, str]) -> Dict[str, Any]:
        with tempfile.TemporaryDirectory() as temp_dir:
            # Initialize temp repository
            subprocess.run(["git", "init"], cwd=temp_dir, check=True)
            # Write original files & commit
            for filename, content in file_context.items():
                (Path(temp_dir) / filename).write_text(content, encoding='utf-8')
            subprocess.run(["git", "add", "."], cwd=temp_dir)
            subprocess.run(["git", "commit", "-m", "initial state"])

            # Verify applicability
            patch_file = Path(temp_dir) / "patch.diff"
            patch_file.write_text(patch, encoding='utf-8')
            res = subprocess.run(["git", "apply", "--check", "patch.diff"], cwd=temp_dir, capture_output=True, text=True)
            return {"applicable": res.returncode == 0, "message": res.stderr.strip()}

2. AST Syntax Validator (ASTValidator)

To prevent the agent from introducing breaking compilation or interpreter bugs:

  • Python: Uses Python’s native ast.parse module to check for syntax validity.
  • JavaScript/TypeScript: Employs a fast, comment-and-string-stripped token-matching bracket scanner to verify that all braces {} and parentheses () are properly closed.

3. File Grounding Validator (FileGroundingValidator)

Prevents model hallucinations by extracting all targeted filenames from the unified diff headers and verifying that they exist within the uploaded source file set.

Interactive Visual Verification (Visual Loop)

Regression Loop

Regression Loop for ‘Split-slider’, Side-by-side’ and Pixel-diff-heatmap’ visuals.

To complete the closed-loop developer experience, the frontend features a premium dashboard tab containing:

  1. Interactive Before/After Split Slider: Let developers scrub a visual slider side-by-side to compare the buggy UI with the expected fix state.
  2. Canvas-Computed Pixel Difference Heatmap: Leverages an HTML5 canvas to compare visual buffers in-browser. It maps changed pixels onto a semi-transparent red overlay and computes an alignment score:
const runPixelDiff = (imgA, imgB, canvas) => {
  const ctx = canvas.getContext('2d');
  const w = canvas.width, h = canvas.height;
  ctx.drawImage(imgA, 0, 0, w, h);
  const dataA = ctx.getImageData(0, 0, w, h);
  ctx.drawImage(imgB, 0, 0, w, h);
  const dataB = ctx.getImageData(0, 0, w, h);

  const diffImg = ctx.createImageData(w, h);
  let changedPixels = 0;
  for (let i = 0; i < dataA.data.length; i += 4) {
    const diffR = Math.abs(dataA.data[i] - dataB.data[i]);
    const diffG = Math.abs(dataA.data[i+1] - dataB.data[i+1]);
    const diffB = Math.abs(dataA.data[i+2] - dataB.data[i+2]);
    if (diffR > 45 || diffG > 45 || diffB > 45) {
      diffImg.data[i] = 255;   // Red highlight
      diffImg.data[i+1] = 0;
      diffImg.data[i+2] = 0;
      diffImg.data[i+3] = 160; // Transparency
      changedPixels++;
    }
  }
  ctx.putImageData(diffImg, 0, 0);
  const score = Math.max(0, 100 - (changedPixels / (w * h)) * 100);
  return score.toFixed(1);
};

📊 Evaluation & Empirical Benchmarks

To validate the agent’s accuracy and reliability, we built an automated, reproducible benchmark framework (backend/benchmark.py). We evaluated the agent across 10 diverse test cases representing real-world frontend and backend bugs:

  1. CSS Overflow Bug: Container text overflowing without truncation controls.
  2. Z-Index Stacking Context: Modal overlay blocking standard content interactions.
  3. Flexbox Alignment Mismatch: Layout components failing to vertically align.
  4. Python AttributeError: Missing None checks on API response payloads.
  5. JS Event Handler Selectors: Target selectors mismatching DOM button bounds.
  6. CSS Contrast Violation: Low-contrast foreground and background colors.
  7. Sidebar Mobile Breakpoint: Layout breaks on smaller screen aspect ratios.
  8. Python Circular Dependency: Circular imports crash during service boot.
  9. SQL Injection Vulnerability: Missing parameter sanitization on user input queries.
  10. JS DOM Selector Mismatch: Target fields mismatching the email form input.

Benchmark Metrics Summary

  • Overall Agent Success Rate: 100.0% (10/10 cases resolved)
  • UI Bug Localization Accuracy: 100.0% (correct root cause selector tracing)
  • Git Apply Applicability Rate: 100.0% (clean, zero-hunk conflict applying)
  • AST / Syntax Validity Rate: 100.0% (zero syntax regression)
  • Average Analysis Latency: 0.90s
  • Average Patch Line Accuracy: 100.0% (identical alignment with human-engineered fixes)

🛠️ Reproducible Quick Start

You can run the entire agentic system and its benchmark suite locally in seconds using Mock Mode (no API keys required)!

1. Install Dependencies

# Clone the repository
git clone [email protected]:kanyingidickson-dev/Multimodal-Visual-Regression-Patch-Agent.git
cd Multimodal-Visual-Regression-Patch-Agent

# Set up virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r backend/requirements.txt

2. Compile Frontend Assets

cd frontend
npm install
npm run build
cd ..

3. Run Benchmark Suite

python3 backend/benchmark.py

This writes the test case directories, triggers the evaluation pipeline, and outputs a complete report inside examples/benchmark-cases/report.md.

4. Run FastAPI Server

python3 backend/app.py

Visit http://127.0.0.1:5000 to start visual regression testing interactively!

You can click ‘Load Example’ on Model settings for a quick demo launch and review.

🔮 The Road Ahead

This project shows what is possible when open multimodal models are coupled with deterministic validation sandboxes. By shifting the paradigm from “AI code review suggestions” to closed-loop visual agentic repair, we are paving the way for developers to resolve UI defects with full safety guarantees in seconds.

Built for the Gemma 4 Challenge:- demonstrating how open, multimodal models can empower developers with intelligent, visual-aware coding tools.

#ai #developertools #gemma4 #multimodal #agentic #patchvalidation #visualregression #opensource #devtools #coding #aiagents #gemma #gemma4challenge #hackathon #openai #google #developerexperience #visual-aware-coding #ai-agents #coding-assistant #visual-regression-patch-agent

Pressure-testing Ota on Supabase: from setup prose to executable repo readiness

Supabase already has strong contributor documentation.

That is not faint praise. The docs are good. But good docs and executable readiness are not the same thing.

Before Ota, the www/docs contributor path still depended on a familiar manual loop:

  • read the docs
  • infer which workflow is the intended front door
  • install the right runtime and package manager versions
  • guess which commands are safe to run
  • decide whether the repo is actually ready, or only partially set up
  • if something fails on macOS, Windows, or Linux, figure out whether the problem is the repo, the machine, CI, or your shell

That is a normal setup experience in modern repos. It is also exactly where drift starts.

This post is about what changed when I added Ota to a scoped slice of the Supabase monorepo, how that improved the repo surface itself, and what the deeper pressure test exposed in Ota core.

Why this matters

Most repos still answer “why does this not run?” badly.

The failure mode usually looks like this:

  • local setup works differently from CI
  • one workflow is implied, but not explicitly declared
  • README setup and repo reality slowly diverge
  • native and container paths are present, but not clearly separated
  • readiness is inferred from “it seems to work” instead of declared checks
  • contributors and agents have to reverse-engineer what the repo expects

That is painful for humans. It is worse for agents.

If an AI agent is dropped into an unfamiliar repo, the first problem is usually not the code change itself. The first problem is operational ambiguity:

  • what this repo slice needs
  • which workflow is the front door
  • which tasks are safe
  • what “ready” means
  • what is actually blocking progress
  • what the next safe action should be

If the repo cannot expose that clearly, the agent is forced to guess from README prose, scripts, lockfiles, CI output, and convention.

That is what Ota is designed to remove.

What Ota added to Supabase

The upstream PR is intentionally scoped and intentionally lean:

The PR adds:

  • an ota.yaml contract for the www/docs slice
  • a cross-OS contract matrix
  • Ota local artifact ignores
  • a small optional Ota section in CONTRIBUTING.md

That gives Supabase a machine-readable contract for:

  • the runtime and toolchain it expects
  • the workflows contributors should use
  • the setup tasks that make those workflows runnable
  • the readiness checks that prove the slice is actually up
  • the tasks and topology surfaces tooling can inspect directly

This is the shift from setup prose to executable repo readiness.

Before and after

Before Ota

The www/docs path was documented, but it was not encoded as repo truth.

That left both contributors and agents to reconstruct:

  • which commands matter
  • which runtime/toolchain versions are expected
  • whether native and container paths are meant to be equivalent
  • what should count as “ready”
  • how to distinguish a local issue from a repo contract issue

After Ota

That same slice can now answer those questions directly.

You can ask the repo:

ota validate .
ota doctor --workflow instant --native .
ota up --workflow instant --native --dry-run .
ota tasks --workflow app .
ota execution topology --json .

And the repo can answer from declared contract truth, not inferred setup lore.

For Supabase, that reduces four kinds of drift:

  1. README drift

    Setup prose can go stale. A contract is executable and testable.

  2. Local vs CI drift

    The same declared workflow can be pressure-tested in matrix CI.

  3. Human vs agent drift

    Humans and agents can consume the same operational truth surface.

  4. Platform drift

    Windows, macOS, and Linux differences get surfaced earlier instead of being rediscovered ad hoc.

Why the upstream PR stayed lean

This part was deliberate.

The public PR models the www/docs slice. It does not claim full Supabase readiness.

That is not a limitation of ambition. It is a review strategy.

A smaller honest contract is easier to review, easier to approve, and easier to trust than a broad fuzzy one. So the public change stays disciplined:

  • scoped slice
  • clear workflow surface
  • concrete CI pressure around that surface

But that did not mean I only did the minimum.

In parallel, I ran a deeper pressure test on a separate branch:

That separation mattered:

  • keep the upstream PR lean for higher approval odds
  • do the heavier product hardening outside the PR
  • fix real Ota gaps in Ota itself instead of hiding them in repo-local glue

That is the right way to use a serious repo as a product test surface.

The pressure test

The first PR branch was only the start.

The full pressure cycle expanded into:

  • native matrix coverage across Ubuntu, macOS, Windows PowerShell, and Windows Git Bash
  • container matrix coverage across supported runner surfaces
  • strict E2E parity checks
  • bounded verification tasks
  • a broader studio slice on the pressure branch
  • repeated reruns to separate repo noise from real Ota defects

Representative runs:

This is where the value of Ota becomes clearer.

The point was not just to prove that ota.yaml could validate.

The point was to prove that the readiness model, workflow targeting, and platform behavior held up under real pressure.

What the contract actually models

At a high level, the contract models:

  • Node + pnpm toolchain expectations
  • native and container execution surfaces
  • workflow-specific setup paths
  • HTTP readiness for www and docs
  • safe bounded tasks for validation and verification

That gives the repo a surface that tools can inspect directly instead of inferring from scattered files.

For example:

ota validate .
ota doctor --workflow instant --native .
ota up --workflow instant --native --dry-run .
ota tasks --workflow app .
ota execution topology --json .

Those commands are not wrappers around documentation. They operate from declared repo truth.

What actually broke in Ota

Supabase exposed a real Ota bug.

The problem was selected workflow toolchain scoping.

In practice, Ota could let unrelated toolchain-owned tools leak into a selected workflow surface just because they were declared elsewhere in the contract.

That sounds small. It is not.

If a selected workflow does not require something, Ota should not silently activate or diagnose against it just because the broader repo declares it somewhere else.

If it does, you get:

  • noisy dry-runs
  • misleading readiness signals
  • false activation pressure
  • lower trust in workflow targeting

That bug was not fixed in the Supabase contract.

It was fixed in Ota core.

That distinction matters. If the platform is wrong, the correct move is to fix the platform, not teach users to work around it.

What did not count as an Ota bug

The pressure test also surfaced failures that looked suspicious at first but were not Ota defects.

The clearest example was the Docker-backed Studio path on hosted CI.

That failure came from runner and stack-specific behavior in the Supabase Docker environment, including hosted ulimit constraints. That is real repo/runtime behavior, but it is not an Ota orchestration bug.

So the right response was:

  • do not misclassify repo or runner behavior as an Ota platform defect
  • do not hide runtime limitations just to force more green boxes
  • keep the matrix useful for signal, not vanity

Trust is not built by making every box green. Trust is built by classifying failures correctly.

What value this adds to Supabase

The value here is not “Supabase now has another YAML file.”

The value is that a real slice of the repo now has:

  • an explicit operational contract
  • declared workflow entry points
  • repeatable readiness checks
  • machine-readable task and topology surfaces
  • cross-platform pressure testing around that declared truth

For Supabase maintainers, that means:

  • less rediscovery of the same setup blockers
  • earlier visibility into platform-specific drift
  • a clearer operational model for contributors

For contributors, that means:

  • less guessing
  • less README archaeology
  • less ambiguity around which path is actually supported

For agents, that means:

  • fewer blind setup guesses
  • a clearer answer to “what should I run?”
  • a more reliable signal for “is this repo ready, or am I about to make changes in a broken environment?”

Why Ota still adds value when a repo already has good docs

A fair question is: if Supabase already has solid docs, what does Ota add?

Docs and contracts do different jobs.

Docs explain.

Contracts declare.

Docs are excellent for onboarding context, caveats, workflow explanation, and human guidance. But docs do not usually answer machine-checkable questions like:

  • is this workflow ready right now?
  • which workflow is the front door?
  • which setup path belongs to this slice?
  • which checks prove readiness?
  • which tasks are safe for agents?
  • how does this differ across native and container execution?

That is the gap Ota fills.

It does not replace good docs. It gives the repo an executable readiness layer that docs alone cannot provide.

Why this matters beyond Supabase

The Supabase case study is really about a broader repo question:

What should happen when a repo does not run?

Most repos still answer that question badly.

They fail, and then they force the contributor to become the diagnosis layer.

Ota changes that shape.

A repo can declare:

  • what it needs
  • how it becomes runnable
  • what ready means
  • what is safe to run
  • what should block progress

That turns readiness from tribal knowledge into contract truth.

Supabase is a useful example because it is large enough to expose real pressure, but still scoped enough to model honestly.

Current state of the PR

The upstream PR is intact:

From our side:

  • no unresolved actionable review threads
  • contract and matrix changes are clean
  • remaining red checks are upstream Vercel authorization and deploy noise, not Ota contract failures

So the Ota side of the work is sound.

Try it on your repo

If your repo already has setup docs, that is a good start.

The harder question is whether the repo can expose, in a machine-readable and testable way:

  • what it needs
  • how it becomes runnable
  • what ready means
  • what is safe to run
  • what should block progress

That is where Ota adds value.

If you want to pressure-test that on a real codebase, start here:

And if you want to start from the Supabase example specifically:

Note: at the time of writing, the upstream Supabase PR is still open. I’ll update this post with the final outcome once maintainers finish review.

References

The concrete artifacts from this work are here:

VPC CNI en EKS: cómo dejar de pagar nodos que no usás

VPC Architecture

Si alguna vez miraste un nodo de EKS y viste algo como esto:

CPU:    18%
Memory: 22%
Pods:   29/29 ← 😐

ya viviste el problema del que va este post. Tus nodos tienen recursos para utilizar pero no podes schedulear más pods en ellos…

La buena noticia: hay una configuración llamada Prefix Delegation en AWS VPC CNI que amplia este techo, y bien configurada te puede bajar la factura de EC2 hasta un 75% sin tocar una sola línea de código de la app.

Vamos por partes.

Cómo funciona el networking de un nodo de EKS

Un nodo de EKS es una instancia EC2. A esa EC2 se le atachan Elastic Network Interfaces (ENIs), que son las placas de red virtuales. Cada ENI tiene:

  • 1 IP primaria (la del nodo en esa ENI)
  • N IPs secundarias (las que se asignan a los pods)

VPC Architecture

El encargado de entregar IPs

El plugin Amazon VPC CNI (que corre como un DaemonSet llamado aws-node en cada nodo) es el encargado de pedirle IPs a EC2 y dárselas a los pods cuando arrancan. Cada pod recibe una IP real y ruteable de la VPC, no una IP virtual dentro de EKS. Lo cual es la causa del problema: el número de pods queda atado a cuántas IPs aguanta la instancia.

Para saber cuantos pods pueden schedulearse en un nodo puede usarse la siguiente formula:

MaxPods x Nodo = ENIs × (IPs_por_ENI − 1) + 2

donde:

  • ENIs: cuántas placas virtuales atacha la instancia
  • IPs_por_ENI − 1: descontamos la IP primaria
  • + 2: dos pods con hostNetwork: true que no consumen IP de pod (típicamente kube-proxy y aws-node)

Los límites de ENIs e IPs por instancia están en la doc oficial de tipos de instancia EC2. Aplicada a la familia M5:

Instancia vCPUs RAM ENIs IPs/ENI Max Pods
m5.large 2 8 GiB 3 10 29
m5.xlarge 4 16 GiB 4 15 58
m5.2xlarge 8 32 GiB 4 15 58
m5.4xlarge 16 64 GiB 8 30 234
m5.8xlarge 32 128 GiB 8 30 234
m5.16xlarge 64 256 GiB 15 50 737

Veamos un ejemplo

Analizando la m5.large: 29 pods. Con 2 vCPU y 8 GiB de RAM, si tus pods piden 100m CPU y 256 MiB de RAM (apis simples), la cuenta da:

  • Por CPU: 2000m / 100m = 20 pods (sin overhead).
  • Por RAM: 8192 MiB / 256 MiB = 32 pods.
  • Por red: 29 pods.

Y restando los pods de sistema (kube-proxy, aws-node, csi-drivers, coredns en algunos nodos), te quedan unos 24 slots útiles. Estás pagando 2 vCPUs y 8 GiB de RAM para correr aplicaciones que apenas usan la mitad del nodo.

Si además estás utilizando escalado automático, tu controller al ver que no entran más pods, levanta otro nodo igual. Y otro. Y otro.

Así aumentan los costos de nuestras EC2.

La solución: Prefix Delegation

Este AWS VPC CNI plugin que analizamos anteriormente puede configurarse en otro modo de funcionamiento (Prefix Delegation).
En este modo, en lugar de pedirle a la EC2 una IP a la vez, el CNI puede pedirle prefijos /28. Es decir, bloques contiguos de 16 IPs. Cada “slot” de la ENI pasa de ser 1 IP a ser 16 IPs.

Entonces, a partir de este cambio tenemos una fórmula nueva:

MaxPods = ENIs × ((IPs_por_ENI − 1) × 16) + 2

Para la misma m5.large ahora tendriamos: 3 × (9 × 16) + 2 = 434 pods teóricos.

Pero esto es más teórico que real. Debajo lo analizamos mejor.

Cómo se activa

Es una variable de entorno en el addon vpc-cni:

{
  "env": {
    "ENABLE_PREFIX_DELEGATION": "true",
    "WARM_PREFIX_TARGET": "1"
  }
}

Y kubelet?

Acá viene la trampa. Activar Prefix Delegation no es suficiente — hay que decirle al kubelet de cada nodo que use el límite nuevo (no lo detecta solo). Si no, vas a tener IPs disponibles pero Kubernetes va a seguir rechazando pods con un mensaje de error: “Too many pods, 0/N nodes are available”.

Para configurarlo:

Si usas Karpenter

Karpenter por default usa --use-max-pods=false y un cap de 110 pods, pero no lo ajusta por instancia automáticamente. Hay que decirle explícitamente en el NodePool:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    expireAfter: Never
  limits:
    cpu: "50"
  template:
    spec:
      kubelet:
        maxPods: 110     # 👈 el cambio

¿Por qué Karpenter no lo hace solo? Porque el script max-pods-calculator.sh de AWS sigue usando la fórmula vieja (Secondary IP) y no entiende Prefix Delegation. Karpenter prefiere dejarte un default conservador y que vos decidas. Info en la doc de Karpenter NodeClass.

Con Managed Node Groups

Hay que ajustar el bootstrap script:

/etc/eks/bootstrap.sh my-cluster 
  --use-max-pods false 
  --kubelet-extra-args '--max-pods=110'

La recomendación/limitación propia de Kubernetes

Antes de poner el resultado de la nueva tabla, hay que tener en cuenta el siguiente thresholds de kubernetes:

min(110, 10*#cores)

La regla, validada por kubernetes, asegura que el promedio de procesos por core quede en un rango que el scheduler del kernel maneje bien.

Entonces el nuevo calculo para pods por EC2 es el siguiente:

maxPods_real = min(kubelet_maxPods, EKS_cap, CNI_formula, 10 × cores)

Para una m5.large entonces con PD: min(110, 250, 434, 20) = 20. Pero ahora tenemos menos. En este caso conviene dejar en: 110.

Referencia: Kubernetes Scalability Thresholds (SIG Scalability).

EC2 vs Pods – Prefix Mod

Ahora revisando nuevamente las EC2:

Instancia vCPUs RAM ENIs IPs/ENI Max Pods (sin PD) Max Pods (con PD)
m5.large 2 8 GiB 3 10 29 110
m5.xlarge 4 16 GiB 4 15 58 110
m5.2xlarge 8 32 GiB 4 15 58 110
m5.4xlarge 16 64 GiB 8 30 234 110
m5.8xlarge 32 128 GiB 8 30 234 250
m5.12xlarge 48 192 GiB 8 30 234 250
m5.16xlarge 64 256 GiB 15 50 737 250
m5.24xlarge 96 384 GiB 15 50 737 250

Cuándo Prefix Delegation NO es la respuesta

Hay casos donde no ganás nada o incluso te complica la vida:

  • Pods grandes (>1 vCPU, >2 GiB de RAM): el cuello ya es CPU/RAM, no IPs. Activar PD no cambia nada.
  • VPC con subnets chicas: un /28 reserva 16 IPs aunque uses 1. Con 10 nodos pidiendo 2 prefijos al boot, eso son 320 IPs apenas el cluster arranca. Una subnet /24 (251 IPs usables) se queda corta. Recomendación: subnets /20 o más grandes para clusters con PD.
  • Instancias no-Nitro: no es compatible. Solo Nitro (m5, c5, r5+, t3, m6i, etc.).
  • Subnets fragmentadas: si la subnet ya tiene muchas IPs sueltas dispersas, EC2 puede no encontrar un /28 contiguo.

La regla práctica: PD es palanca para instancias chicas con pods chicos. Para instancias grandes con pods pesados, el problema ya es otro.

Pensando en costos:

Caso de analisis: cluster con ~200 pods de microservicios livianos (100m CPU / 256 MiB RAM cada uno).

Sin Prefix Delegation, con m5.large:

  • 24 pods de app por nodo (descontando 5 de sistema)
  • 200 / 24 = 9 nodos
  • 9 × US$ 0.096/h × 730 h = US$ 631/mes

Con Prefix Delegation, con m5.xlarge y maxPods=110:

  • ~104 pods de app por nodo
  • 200 / 104 = 2 nodos
  • 2 × US$ 0.192/h × 730 h = US$ 280/mes

Ahorro: ~55% sobre el costo de cómputo, siendo conservador. Se pueden disminuir más los costos usando instancias SPOT.

Conclusión

El networking de EKS tiene un techo que es invisible hasta que se analizan los costos. Prefix Delegation no es un cambio gigante, es una variable de entorno que te baja el costo de cómputo en ciertos casos de usos.

Material de referencia

Este post nace de mi charla en AWS Community Day Argentina 2025. Si tenés dudas sobre cómo aplicar esto a tu cluster contactame en /in/blanco-lucas.

The Future of Text Analysis: Introducing TechnoHelps Semantic Engine

The Cost of Black-Box Analytics

As a developer, I have always been fascinated by how we process data. When I started building my own content platforms, I hit a wall that many of you probably recognize: the “Black Box” problem of analytics.

Most text analysis tools on the market treat developers like end-users. You input text, you get a “Readability Score,” and you move on. But for a developer building a product, that’s not enough. We need transparency. We need to know how that score was calculated, why certain keywords were prioritized, and where the data is flowing. Existing enterprise APIs either cost a fortune or offer zero insight into the logic behind the metrics.

That frustration became the foundation for the TechnoHelps Semantic Engine.

Our Vision: Democratizing Developer Infrastructure

TechnoHelps is built on a single, unwavering philosophy: Infrastructure should be accessible. In an era where “SaaS” often means “Subscription-as-a-Service,” I wanted to build something different. Our vision is to provide enterprise-grade, high-performance analytical tools that are free and open-source. Whether you are a solo developer building a niche app or a team scaling a publication, you deserve the same analytical infrastructure as the industry giants. We aren’t just building a tool; we are building a foundation for a more transparent and open web.

The Technical Edge: Why This API Matters

The TechnoHelps Semantic Engine isn’t just another string-matching algorithm. It is a purpose-built semantic analyzer designed for performance and clarity.

Precision Readability Scoring:** We utilize the Flesch Reading Ease algorithm, but we provide it with a level of granularity that helps you understand not just if your text is “easy,” but why it is easy based on syllable-to-word ratios.

Contextual Keyword Extraction: SEO tools often spam keywords. Our Keyword Matrix uses intelligent stop-word filtering to ensure that you are tracking the thematic density that actually matters to your content strategy.

Algorithmic Velocity Metrics: We have developed custom functions to calculate precise reading and speaking times. It’s a small detail, but for user retention and UX, knowing exactly how long your audience spends with your content is a massive competitive advantage.

An Open Source Commitment

I firmly believe that the best code is code that can be audited, improved, and owned by its users. That is why I have made the entire Semantic Engine project Open Source.

You can view, fork, and contribute to the source code on our GitHub Repository. If you find a bug, propose an improvement, or want to add a feature—it’s all in your hands. My goal is to ensure this engine evolves with the developer community, not in isolation.
A Message to the Community

The TechnoHelps Semantic Engine is 100% free to use. I built this to solve a real-world problem I faced daily. While the code is free, building and maintaining these tools takes resources. If you find value in this engine, I would sincerely appreciate an attribution or a shout-out. It helps the project grow and allows me to dedicate more time to building more open-source tools for you.

We are just getting started. This is the first step toward a more powerful, transparent suite of tools for the modern web developer. Join us, use the API, and let’s build something better together.

Founder & Developer’s Note:

Building tools that simplify the web is my passion. The TechnoHelps Semantic Engine is the culmination of months of engineering, and I am proud to share it with you. If you have feedback, reach out or contribute to our GitHub repo.

Shahab Khan
Founder & Lead Developer, TechnoHelps
Follow our engineering journey at TechnoHelps.com

I built a Chrome Extension that saves product images + context directly to Google Drive & Sheets

I built a Chrome Extension that saves product images + context directly to Google Drive & Sheets

Most product research workflows are secretly broken. You just don’t notice until you’re drowning in 400 unnamed screenshots and a half-filled spreadsheet at 11pm.

Four problems. One tool.

❌ Manual saving steals hours you don’t have
❌ Scrapers are unstable and dump garbage images with zero context
❌ Images without context become completely unsearchable over time
❌ Your workflow breaks the moment you switch devices

Here’s how ImageSnap solves each one.

❌ Pain #1: Manual saving is a productivity black hole

The typical workflow:

  1. Find a product image you want
  2. Right-click → Save As
  3. Rename the file so it makes sense later
  4. Navigate to the right folder
  5. Open your spreadsheet
  6. Manually type in the product name, price, URL, supplier, notes…
  7. Repeat. 50 times. Every session.

You’re not doing research. You’re doing data entry.

✅ How ImageSnap fixes it: One click opens a side panel. The product name, price, and URL are already filled in — scraped automatically from the page. Hit Snap and the image goes to Google Drive, the row goes to Google Sheets. The whole thing takes 5 seconds.

❌ Pain #2: Scrapers are unstable and give you garbage

Automated scrapers sound great in theory. In practice:

  • They break every time the site updates its HTML
  • They pull hundreds of irrelevant images — icons, banners, ads, lazy-loaded placeholders
  • You get a dump of raw data with no context about why you saved anything
  • Running them requires technical setup most researchers don’t want to deal with

✅ How ImageSnap fixes it: You stay in control. You browse naturally, and when you find something worth saving, you capture it intentionally. No garbage images. No broken selectors. No maintenance. You decide what gets saved and why.

❌ Pain #3: Images without context become unsearchable

This is the silent killer. You save 300 product images over two weeks. Then you need to find “that ceramic supplier from last Tuesday with the MOQ under 500.”

Good luck.

A folder of IMG_20260521_143022.jpg files tells you nothing. Even if you renamed them, you’ve lost the price, the URL, the supplier name, the category — everything that made that image worth saving in the first place.

✅ How ImageSnap fixes it: Every image is captured with structured metadata — price, URL, title, custom fields you define per category. It all lives in Google Sheets, which means you can filter, sort, and search across everything instantly. The image and its context are permanently linked by ID. You’ll find that ceramic supplier in 10 seconds.

❌ Pain #4: Your workflow breaks the moment you switch devices

You save images on your laptop. You spot products on your phone during lunch. You reference your research on a tablet in a meeting. Three devices, three separate systems, constant friction.

✅ How ImageSnap fixes it: Everything syncs to your Google Drive and Google Sheets in real time. Open your sheet on any device — phone, tablet, laptop — and your entire research library is there, with images and context intact. The extension works on desktop. The web dashboard works everywhere. One workflow, every device, any time.

The technical side (for the devs)

The project is a monorepo with two independent build targets:

  • Web dashboard → Next.js 15 + React 19 + Tailwind CSS 4
  • Chrome extension → Vite + TypeScript, Manifest V3

Both share a src/shared/ layer — same auth logic, type definitions, and Google API helpers across both contexts.

src/
├── shared/        ← shared between web + extension
│   ├── lib/       ← google-auth, sheets, drive helpers
│   └── hooks/     ← useAppData, etc.
├── web/           ← React app (popup + web dashboard)
└── extension/     ← manifest, content_script
app/               ← Next.js routes + API

One thing that caught me off guard: extension popups are completely stateless. Every time the popup closes, the JS context is destroyed — including any in-memory auth tokens.

My first instinct was to use an httpOnly session cookie via the API. It silently broke because cookies with sameSite: lax don’t get sent in cross-origin fetch calls from the chrome-extension:// protocol. The fix was chrome.storage.local:

// After successful login
saveTokenToExtStorage(token, profile.email);

// On every popup reopen — no server roundtrip needed
const stored = await loadTokenFromExtStorage();
if (stored?.token) {
  const profile = await getUserInfo(stored.token);
  if (profile) { /* restore state instantly */ }
}

On the data side, ImageSnap stores nothing on its own servers except subscription status. Your images live in your Google Drive. Your metadata lives in your Google Sheets. You own everything.

Try it

ImageSnap is live on the Chrome Web Store with a free tier (30 captures/month).

👉 imagesnap.cloud

If you’re doing ecommerce research, sourcing, competitor tracking, or content creation — give it a try and let me know what you think in the comments.

95+ browser-based dev tools that never touch a server

Every developer has a folder of bookmarks for one-off tools —
a JWT decoder here, a Base64 encoder there, a cron expression
tester somewhere else. Half of them send your data to a server
you know nothing about.

I built Wizbit.to to fix that.

What it is

95+ free developer utilities, all running 100% in your browser.
No server ever sees what you paste. No account. No ads. No limits.

What’s inside

Data & encoding

  • JSON formatter, beautifier, minifier & validator
  • Base64 encoder/decoder
  • CSV → JSON, CSV → Markdown, YAML ↔ JSON converters
  • ENV file parser and ENV → JSON/YAML converter

Security & crypto

  • JWT decoder with expiry checker and claims differ
  • Hash generator (SHA-256, SHA-512)
  • RSA key pair generator & X.509 certificate inspector
  • BIP39 seed phrase generator
  • Password generator & strength checker

Developer utilities

  • Regex tester with live match highlighting
  • Cron expression builder and syntax decoder
  • SQL formatter
  • Webhook visualizer and Stripe webhook simulator
  • UUID/GUID generator (v4, v5, v7, bulk)
  • HTTP status explorer
  • CIDR subnet calculator
  • Semver comparator

Frontend & CSS

  • SVG → JSX converter and SVG cleaner
  • CSS shadow, clip-path, and glassmorphism generators
  • Color converter (HEX, RGB, HSL)
  • Bezier curve and spring physics playground

AI & LLM tools

  • Prompt tokenizer and token calculator
  • Context trimmer and prompt compressor
  • AI response cleaner and text humanizer

VFX & camera (yes, really)

  • Depth of field visualizer
  • LUT .cube file 3D visualizer
  • ACES color space matrix calculator
  • Timecode and frame arithmetic calculator
  • Focal length / FOV / crop factor matcher

Why it’s different

Most online dev tools are thin wrappers around a server call.
That’s fine for public data — but when you’re pasting a JWT,
an RSA private key, an ENV file, or a seed phrase, you’re
trusting a server you’ve never audited.

Wizbit processes everything locally. The code runs in your tab.
Close the tab and it’s gone.

Ghost Bugs Cost $40K: A Neural Debugging Postmortem

When AI silently fails for weeks

A production RAG system handling 12,000 queries/day recently ran for three weeks delivering silent errors, resulting in an estimated $40K in flawed decisions before anyone noticed.

The issue wasn’t a crash or a syntax error. It was vector embedding drift—a silent failure state where the system returned incorrect results that appeared entirely plausible on the surface.

These are often called “ghost bugs.” They don’t throw runtime exceptions, they don’t trigger error logs, and they typically pass standard unit tests. Below is an analysis of how this happens, how to identify it, and how to build a monitoring system to catch it.

Tool: Debug your vectors with Vector Distance Calculator

The $40K mistake

In this case study, the RAG pipeline recommended suppliers based on vector search:

query → vector search → top 3 results → LLM ranking.

For three weeks, the system recommended “Supplier B” over “Supplier A,” even though Supplier B was 23% more expensive. The team trusted the outputs because they looked structurally correct.

The root cause was simple: the embedding model was updated from text-embedding-3-small to text-embedding-3-large, but the older documents in the database were never re-indexed. The vectors lived in entirely different dimensional spaces, but the database didn’t reject the query.

// The silent failure
const queryVector = await embed(query, 'large'); // 3072 dims
const results = await db.search(queryVector);
// Returns vectors from 'small' model (1536 dims)
// Database pads with zeros. No error. Wrong results.

Calculating cosine similarity between mismatched dimensions still returns a mathematical output—it is simply the wrong output.

Ghost bug #1: Dimensional drift

This is a common RAG failure mode. To prevent this during deployments, systems should validate dimensions programmatically:

import { cosineSimilarity } from './vector-utils';

async function detectDimensionalDrift() {
  const testQuery = "test document for embedding";

  // Embed with current model
  const currentEmbedding = await embed(testQuery);

  // Check database sample
  const sample = await db.getRandomVector();

  if (currentEmbedding.length !== sample.vector.length) {
    throw new Error(
      `DIMENSIONAL MISMATCH: Current=${currentEmbedding.length}, DB=${sample.vector.length}`
    );
  }

  // Also check distribution
  const similarity = cosineSimilarity(currentEmbedding, sample.vector);
  if (similarity > 0.99) {
    console.warn('Suspiciously high similarity – possible duplicate model');
  }
}

Running this check as part of continuous integration or deployment pipelines can prevent dimensionality mismatches entirely.

Tool: Test embeddings with RAG Chunk Simulator

Ghost bug #2: The chunk boundary failure

Standard RAG practices often involve chunking documents at fixed limits (e.g., 512 tokens). However, key context can easily be split across chunk boundaries, rendering the retrieved data incomplete.

Example:

  • Chunk 1: “Supplier A: $100/unit. Terms: Net 30.”
  • Chunk 2: “Excludes bulk discount of 40% for orders >1000 units.”
  • Query: “cheapest supplier for 2000 units”

The system retrieves Chunk 1 because it mentions the price, but misses Chunk 2 because the discount details fell into a separate vector. As a result, the model recommends the wrong supplier.

The solution: overlapping chunks with metadata

function smartChunk(text, size = 512, overlap = 128) {
  const chunks = [];
  const sentences = text.split('. ');

  let currentChunk = '';
  let currentSize = 0;

  for (const sentence of sentences) {
    const tokens = estimateTokens(sentence);

    if (currentSize + tokens > size) {
      // Save current chunk with overlap metadata
      chunks.push({
        text: currentChunk,
        metadata: {
          has_continuation: true,
          next_chunk_preview: sentences.slice(0, 3).join('. ')
        }
      });

      // Start new chunk with overlap
      const overlapText = currentChunk.split(' ').slice(-overlap).join(' ');
      currentChunk = overlapText + ' ' + sentence;
      currentSize = estimateTokens(currentChunk);
    } else {
      currentChunk += ' ' + sentence;
      currentSize += tokens;
    }
  }

  return chunks;
}

Ghost bug #3: Temperature creep

LLM parameters are highly sensitive. A configuration meant for creative tasks can cause hallucinations if applied to analytical ranking tasks.

Consider a configuration setup like this:

// config.js
export const LLM_CONFIG = {
  temperature: process.env.LLM_TEMP || 0.7,
  //...
};

If an environment variable in production is accidentally modified (e.g., setting LLM_TEMP=1.2 for testing without reverting it), the model can produce highly inconsistent or hallucinated supplier rankings without throwing a system error.

The solution: runtime configuration validation

function validateLLMConfig(config) {
  const issues = [];

  if (config.temperature < 0 || config.temperature > 1) {
    issues.push(`Temperature ${config.temperature} out of bounds [0][1]`);
  }

  if (config.temperature > 0.3 && config.use_case === 'ranking') {
    issues.push('High temperature for ranking task – expect inconsistency');
  }

  // Check for drift from baseline
  const baseline = 0.7;
  if (Math.abs(config.temperature - baseline) > 0.2) {
    issues.push(`Temperature deviated >0.2 from baseline`);
  }

  if (issues.length > 0) {
    throw new Error(`LLM Config Validation Failed:n${issues.join('n')}`);
  }
}

Tool: Validate your JSON configs with JSON Validator

Building a neural debugger

Traditional debuggers are not designed to inspect vector spaces. Monitoring a production RAG system requires tracking the invisible metrics of embeddings and distributions.

Component 1: Embedding fingerprinting

You can verify the stability of your embedding space by regularly testing a static group of phrases:

async function createEmbeddingFingerprint() {
  const testPhrases = [
    "the quick brown fox",
    "supplier pricing data",
    "technical specifications",
    "random unrelated text about cats"
  ];

  const fingerprints = await Promise.all(
    testPhrases.map(async phrase => ({
      phrase,
      vector: await embed(phrase),
      hash: simpleHash(await embed(phrase))
    }))
  );

  await db.saveFingerprint({
    timestamp: Date.now(),
    model: EMBEDDING_MODEL,
    fingerprints
  });

  return fingerprints;
}

// Check for drift on a daily schedule
async function checkForDrift() {
  const baseline = await db.getLatestFingerprint();
  const current = await createEmbeddingFingerprint();

  for (let i = 0; i < baseline.fingerprints.length; i++) {
    const similarity = cosineSimilarity(
      baseline.fingerprints[i].vector,
      current[i].vector
    );

    if (similarity < 0.95) {
      alert(`EMBEDDING DRIFT DETECTED: ${baseline.fingerprints[i].phrase}`);
    }
  }
}

Component 2: Query-result monitoring

Analyzing the distributions of search scores helps flag anomalies before they impact users:

async function monitorQueryQuality(query, results) {
  const metrics = {
    query,
    timestamp: Date.now(),
    resultCount: results.length,
    avgSimilarity: results.reduce((sum, r) => sum + r.score, 0) / results.length,
    topScore: results[0]?.score || 0,
    scoreVariance: calculateVariance(results.map(r => r.score))
  };

  if (metrics.avgSimilarity < 0.7) {
    logWarning('Low similarity scores – possible embedding mismatch');
  }

  if (metrics.scoreVariance < 0.01) {
    logWarning('All scores nearly identical – possible dimensional issue');
  }

  if (metrics.topScore > 0.99) {
    logWarning('Suspiciously perfect match – check for data leakage');
  }

  await db.logMetrics(metrics);
}

Component 3: The “Golden Query” set

Run automated tests against queries with static, known correct answers to evaluate performance continuously:

const GOLDEN_SET = [
  {
    query: "cheapest supplier for bulk orders",
    expected_top_result: "supplier-a",
    min_similarity: 0.85
  },
  {
    query: "supplier with fastest delivery",
    expected_top_result: "supplier-c",
    min_similarity: 0.80
  }
];

async function runGoldenTests() {
  const failures = [];

  for (const test of GOLDEN_SET) {
    const results = await ragSearch(test.query);
    const topResult = results[0];

    if (topResult.id !== test.expected_top_result) {
      failures.push({
        test: test.query,
        expected: test.expected_top_result,
        got: topResult.id,
        similarity: topResult.score
      });
    }

    if (topResult.score < test.min_similarity) {
      failures.push({
        test: test.query,
        issue: 'low_similarity',
        score: topResult.score,
        threshold: test.min_similarity
      });
    }
  }

  if (failures.length > 0) {
    await alertEngineering('GOLDEN TEST FAILURES', failures);
  }

  return failures.length === 0;
}

// Execute tests at regular intervals (e.g., every 5 minutes)
setInterval(runGoldenTests, 5 * 60 * 1000);

Typical metrics after implementation

Implementing these checks transitions a team from reactive troubleshooting to proactive observability:

Metric Unmonitored Monitored
Detection Time Weeks (or never) Minutes
Silent Failures Caught None High
Engineering Stance Reactive Proactive

Tool: Format your debug logs with JSON Formatter

Auditing your RAG setup

If you are running a production RAG system, a quick baseline audit is highly recommended:

  1. Verify dimensions: Ensure all stored vector spaces strictly match your runtime embeddings model.
  2. Establish golden tests: Set up a list of key queries with deterministic target results.
  3. Audit similarity distribution: Look for unexpectedly flat or perfect similarity scores.
  4. Enforce configuration schema: Guard runtime variables like temperature with schema validation.

RAG applications operate on non-deterministic models. If you are not monitoring the hidden parameters of your vector spaces, you may be missing critical bugs.

Have you encountered silent errors in your AI applications? Share your debugging strategies or experiences in the comments below.

Tools referenced in this post:

  • Vector Distance Calculator
  • RAG Chunk Simulator
  • JSON Validator
  • JSON Formatter

Read more: Debugging RAG Vector Distance

SECPAC: A Lightweight CLI Tool to Password-Protect Your Environment Variables 🚀

If you are tired of dealing with plain-text .env files and manual editing, SECPAC is a lightweight Node.js CLI tool built to bring structured management and optional password encryption to your local environment variables. By serving as a faster, more secure alternative to traditional setups, it allows you to easily view, grab, or mask sensitive API keys directly from your terminal. Check out the project, join our growing community at r/secpac, and let me know how it improves your development workflow!

PasteCheck v1.7 + v1.8 — Hints that tell you what to fix, and a nudge panel that tells you where to start

Most linters tell you what’s wrong. They stop there.

PasteCheck has always shown you the error and the line. But “var is discouraged” doesn’t tell a beginner what to actually do about it. “Empty catch block” doesn’t tell you why it matters or what to replace it with.

That changes in v1.7.

Every error and warning message now ends with one actionable sentence. Not a docs link. Not a vague suggestion. One clear fix.

  • Prefer 'let' or 'const' over 'var' — use 'const' if the value never changes, 'let' if it does
  • 'eval()' executes arbitrary code — dangerous and a security risk — remove it and find a safer alternative like JSON.parse() or a lookup object
  • Empty catch block — errors are silently swallowed — add at least console.error(e) so failures don't disappear silently

This covers every language PasteCheck supports — JavaScript, TypeScript, Python, HTML, and CSS. 30 messages updated across the entire linter.

Two Reddit commenters validated this independently before it was built. Both said the same thing unprompted — knowing what’s wrong isn’t enough, you need to know what to do next. That’s what v1.7 ships.

v1.8 adds something no other paste tool has.

When errors or warnings exist, a collapsible “What to do next” panel appears below the results. Not per-error guidance — big picture debugging logic. The kind of thing an experienced developer would tell you if they were sitting next to you.

  • If you have errors: fix the first one before the rest. Errors cascade. One missing bracket can cause five false positives below it.
  • If you have warnings only: no errors is good — work through warnings one at a time using the fix hints above.
  • If you have both: ignore the warnings for now. Fix the errors first. Warnings can wait until your code runs.

It’s collapsed by default. It doesn’t get in the way. It’s there when you need it.

Both features were designed around the same principle — PasteCheck doesn’t just find your mistakes, it helps you understand them.

Built entirely from an Android phone. Live at pastecheck.co.uk.

8 Real Ways Developers Make Money in 2026 (Ranked by Effort)

Every “make money as a developer” post promises passive riches by Friday. This isn’t that.

I’ve tried most of these. Some paid rent. Some paid for coffee. One quietly pays me while I’m asleep. Below is an honest ranking — sorted from “most effort for the money” to “least” — with realistic timelines and the catch for each, because the catch is the part nobody writes about.

1. Freelancing / contracting

Effort: High · Time to first dollar: 1–6 weeks · Ceiling: Very high

Still the fastest reliable path. Upwork and Fiverr are crowded races to the bottom; the money is in Toptal, Contra, Gun.io, or — best of all — your own network. Your second client should come from your first client’s referral, not a bidding war.

The catch: You’re trading hours for money, plus the unpaid hours of finding clients, scoping, and chasing invoices. It’s a business, not a side hustle. Treat it like one or it’ll treat you like one.

Tip: Niche down. “React developer” competes with 200,000 people. “I rescue stalled Next.js migrations for B2B SaaS” competes with almost none and charges 3x.

2. Build and sell digital products

Effort: High up front, low after · Time to first dollar: 1–3 months · Ceiling: High

Boilerplates, UI kits, Notion templates, VS Code themes, icon sets, Figma files, code generators. Build once, sell forever. Gumroad, Lemon Squeezy, and the GitHub Marketplace make distribution trivial.

The winners solve a boring problem you’ve already solved for yourself. Auth boilerplate. A Stripe + webhooks starter. A deploy script you keep copy-pasting.

The catch: “Build once” is a lie — you’ll support, update, and market it forever. Distribution is 80% of the work and writing the code was the fun 20%.

3. Micro-SaaS / indie hacking

Effort: Very high · Time to first dollar: 3–12 months · Ceiling: Life-changing

The dream: a small product with recurring revenue you own end to end. Totally real — see the indie hackers shipping $5–50k/month tools. Also where most developers quietly burn six months and ship to crickets.

The catch: The hard part isn’t the code, it’s finding a problem people will pay for before you build. Talk to users first. Charge from day one. If nobody pays for the landing page, nobody pays for the product.

4. Teaching: courses, YouTube, cohorts

Effort: High · Time to first dollar: 2–6 months · Ceiling: High

Udemy, YouTube, Egghead, or your own cohort-based course. You don’t need to be the world’s best engineer — you need to be one chapter ahead of your audience and good at explaining.

The catch: It compounds slowly and publicly. Your first 20 videos will flop. The audience is the asset, and audiences take a year+ to build.

5. Technical writing

Effort: Medium · Time to first dollar: 2–4 weeks · Ceiling: Medium

Companies pay $200–$1,000+ per article for quality developer content. Dev tool companies (databases, auth, observability, anything DX-focused) constantly need writers who can actually code. Check Draft.dev, the DevRel job boards, or just pitch companies whose tools you use.

The catch: You need writing samples to get hired to write. Publish a few strong posts (like this one) on your own blog or dev.to first, then point to them.

6. Open source sponsorship

Effort: Medium-high · Time to first dollar: Slow · Ceiling: Medium

GitHub Sponsors, Open Collective, Polar. If you maintain something people depend on, companies and individuals will fund it. Some maintainers fund full-time work this way.

The catch: Sponsorship follows traction, not the other way around. This pays because your project is useful to thousands, which is years of unpaid work first. Treat it as upside on work you’d do anyway, not a plan.

7. Bug bounties

Effort: High · Time to first dollar: Unpredictable · Ceiling: Very high

HackerOne, Bugcrowd, Immunefi (the last one pays eye-watering sums for smart-contract bugs). If you genuinely enjoy breaking things, the top hunters out-earn senior salaries.

The catch: Brutally feast-or-famine and steep learning curve. You can spend 40 hours and find nothing, or one hour and find $20k. Most people find nothing for a long time.

8. Get paid for your profile — even when you’re not job hunting

Effort: Low · Time to first dollar: Days · Ceiling: Modest (but truly passive)

Here’s the one most developers haven’t tried, and the reason I wrote this list.

Think about your last job search. If someone had paid you $100/hour to do it, what would you have earned? Probably thousands. Instead, companies pay for recruiters, job boards, and ads to find you — and you, the actual product, get nothing.

A newer platform called Timofi flips that. You record a short set of clips — basically a mini interview (how you think, a project you’re proud of, a tradeoff you’d defend) — and drop one link on your LinkedIn, GitHub, or resume. When a recruiter wants to unlock it and actually see you talk, they pay a small fee, and you keep 60%.

The part that sold me: it works even when you’re not looking for a job. If you’re a solid engineer, recruiters already DM you constantly and you ignore them. This makes the good ones pay for your attention. Worst case, someone unlocks, watches, and nothing comes of it — you still got paid for the time. The downside has a floor.

The honest catch: It’s early. The broader hiring marketplace is rolling out gradually, and your earnings scale with how much profile traffic you actually have — so it pays best if you already get inbound from recruiters. It won’t replace your salary. Think “found money on traffic you were ignoring anyway,” not “quit your job.” Setup takes a few minutes, so the effort-to-upside ratio is hard to argue with.

Full disclosure for the dev.to crowd: I think this category — getting paid for your own attention — is genuinely new, which is why it’s on the list. Try it, don’t try it, but the framing alone is worth stealing.

So which one should you actually pick?

  • Need money this month? → Freelancing (#1) or technical writing (#5).
  • Want something that compounds? → Products (#2), teaching (#4), or micro-SaaS (#3).
  • Want upside on work you already do? → Open source sponsorship (#6) or your profile (#8).
  • Love breaking things? → Bug bounties (#7).

The meta-lesson across all eight: stop giving your work, your time, and your attention away for free. Whether you package it as a product, a course, an article, or a paid mini interview — the developers who make money are the ones who decided their output has a price tag.

Which of these have you tried? What worked, what flopped? Drop it in the comments — genuinely curious what’s paying for the dev.to crowd in 2026.