Why your word error rate (WER) benchmark might be lying to you

At AssemblyAI, we’ve spent years helping customers evaluate speech-to-text performance. Word Error Rate (WER) has long been the gold standard metric for this — and for good reason. It’s simple, reproducible, and gives you a number you can compare across vendors.

But something unexpected happened when we launched Universal-3 Pro, our most advanced transcription model to date. Some customers came back to us saying their benchmarks showed the new model performing worse than our older models. That didn’t match anything we were seeing internally.

So we dug in. What we found changed how we think about transcription evaluation entirely.

How traditional WER benchmarking works

Before we get into the issue, let’s level-set on the standard evaluation process — because understanding how it works is key to understanding what’s breaking down.

Here’s the typical workflow:

  1. Collect 10–20 audio files representative of your core use cases.
  2. Submit them to a human transcription provider to get a high-quality ground truth (or “truth file”).
  3. Submit the same audio files to multiple speech-to-text vendors to get AI-generated transcriptions.
  4. Normalize both sets of transcriptions using the Whisper Normalizer — an open-source Python package that lowercases everything, strips punctuation, and maps semantically equivalent words to a common form (e.g., don’t → do not).
  5. Run WER evaluation using an open-source Python package like jiwer, which compares your normalized truth file against your normalized AI predictions.
    WER is simply the percentage of words in the audio that were transcribed incorrectly. Lower is better.

This process has been the primary evaluation method for the better part of a decade. It’s how customers have compared vendors, justified switching decisions, and tracked model improvements over time. And every year, models have gotten better, and model WER continues to decrease over time.

The three types of WER errors

When you run a WER evaluation, errors fall into several categories. The three most common are:

  • Insertions (Hallucinations): Words the AI added that weren’t in the human transcript.
  • Deletions: Words present in the human transcript that the AI missed entirely.
  • Substitutions: Words where the AI transcribed one word instead of another. For example, a human said “My Cadillac isn’t doing great” but the AI transcribed “My cataracts aren’t doing great.” Same number of words — very different meaning.
    Substitutions are particularly problematic downstream, because if you’re feeding transcriptions into a large language model or any AI-based speech understanding tools, a substitution like Cadillac → cataracts doesn’t just create a transcription error. It corrupts the contextual information entirely.

That’s why one of our primary focuses when building Universal-3 Pro was reducing these kinds of errors — specifically around what we call Missed Entity Rate: how accurately the model transcribes rare and high-value words like proper nouns, company names, medication names, alphanumerics, phone numbers, dates, and addresses.

What’s actually happening with Universal-3 Pro

So back to the mystery. Customers were seeing worse WER numbers with our new model. We were confident the model was better. What’s happening here?

When we started reviewing customer benchmarks in detail, we noticed something striking: a disproportionate number of insertions. Not deletions, not substitutions — insertions. The new model was inserting words that weren’t in the human truth files.

So we did something simple: we went back and actually listened to those insertions in the original audio.

The vast majority of them were correct.

The AI was transcribing words that were genuinely spoken in the audio — words that the human transcriptionist had missed. On difficult audio files (noisy environments, accented speech, overlapping speakers, fast talkers), our model was frequently outperforming the human-labeled transcription it was being evaluated against.

This creates a fundamental problem with the traditional WER evaluation framework: if your ground truth is wrong, your evaluation is wrong. And the better your AI model gets, the more this problem is exposed.

A second issue: Semantic equivalence

There’s a related but distinct issue we also uncovered around substitutions.

Because Universal-3 Pro has a large language model decoder at its core, it has a much stronger command of grammar and formatting than traditional transcription models. In practice, this means it sometimes transcribes things correctly in ways that differ from how a human transcriptionist formatted the same content.

A few common examples: a human transcriptionist might write “all right” (two words) while our model writes “alright” (one word). Or a human writes “health care” while our model writes “healthcare.” A third case is compound words — a human might write “set up” as two words while our model correctly writes it as “setup.” The meaning is identical across all three cases — but in a WER evaluation, each one registers as an error.

The Whisper Normalizer handles some of this automatically (the don’t/do not case, for example), but it can’t cover every domain-specific or stylistic variation that might appear in your audio.

The tool we built to fix this

We recognize that asking customers to manually audit every error in their truth files is an unreasonable ask, especially when you’re dealing with hour-long recordings. So we built a tool to make this fast and systematic.

Here’s what it does:

1. Truth File Correction

You submit your existing truth files and your audio. The tool runs a transcription with Universal-3 Pro and surfaces every discrepancy between the AI output and your human truth file — one by one, with the corresponding audio segment playable inline.

For each discrepancy, you click one of three options:

✅ AssemblyAI got this right — update the truth file to match.
❌ AssemblyAI got this wrong — keep the truth file as-is.
✏️ Neither is correct — write in what was actually spoken. (You can also label segments as [inaudible] where the audio is genuinely unclear.)
As you work through the errors, the WER updates live in the tool so you can see the impact in real time. You only have to do this once — and the corrected truth files can be reused for all future benchmark evaluations, including competitive comparisons against other vendors.

2. Semantic Word List Generation

For the formatting and equivalence issues described above, the tool lets you build a semantic word list — essentially an array of word groupings that should be treated as equivalent in your evaluations.

Once you’ve built this list, you can port it into your evaluation pipeline so that these stylistic differences don’t penalize any vendor unfairly. It’s basically a simple find & replace built into your evaluation pipeline for words that are semantically the same. This gives you cleaner, more meaningful WER numbers across the board.

Note: This tool is not yet available. We are working to build this tool directly into our dashboard for your use. It should be available within the next week.

A note on benchmarking streaming vs. async models

One quick aside for teams doing more sophisticated benchmarks: async and streaming models should be evaluated separately. Async models are straightforward — send the full audio file, get back a complete transcript. Streaming models require you to feed audio bytes in real time, which means your benchmark has to run in parallel for the full duration of each audio file. It’s significantly more complex to set up correctly.

Universal-3 Pro is available in both async and streaming form. If you’re benchmarking both, make sure you’re not mixing results.

The GitHub repository

Alongside this tool, we’re releasing a GitHub repository that shows you how to build your own internal benchmarking pipeline using corrected truth files and semantic word lists. Whether you want to run evaluations on a schedule, integrate WER tracking into your CI/CD pipeline, or run competitive benchmarks across multiple vendors, the repo gives you a solid starting point.

The bigger picture

WER has served the industry well. It’s still a useful metric and we’re not suggesting you abandon it.

But the industry is at an inflection point. Models are getting good enough — in some cases better than human transcriptionists on difficult audio — that the traditional evaluation framework is starting to show its age. When your ground truth has more errors than your AI, the number your benchmark produces isn’t just unhelpful. It’s actively misleading.

The right response isn’t to throw out metric-based evaluation. It’s to make the evaluation more accurate. That means better truth files, smarter normalization, and a clearer understanding of what WER is and isn’t measuring.

That’s what we’re helping our customers with every day! If you’re interested in learning more, please reach out to the AssemblyAI team!

Why a Single Markdown File Can’t Be Your AI Agent’s Memory

A blunt reality check from the front lines of AI coding.

On the Cursor forum, a developer asked why .cursorrules kept being ignored. The AI’s reply was painfully direct: “Even if you add Cursor Rules, they are inherently meaningless. I can choose to ignore them. Rules are just text, not enforced behavior.”

That exchange captures a frustration every developer using mainstream AI coding agents has felt. Every major tool does the same thing: uses a static text file as memory. Simple? Yes. Easy to start with? Definitely. But it silently breaks as your project grows, and you pay for that failure with hours of your time.

What Markdown Gets Right

Let’s be fair—Markdown genuinely works in the early stages:

  1. Zero Infrastructure: Just one file in your repo.
  2. Git Managed: Versioning and PR reviews come for free.
  3. Total Transparency: Open the file, and you know exactly what the agent sees.

For stable, long-term rules like “use TypeScript” or “write tests with pytest,” a Markdown file is fine.

The problem is that projects evolve. They don’t stay simple. And Markdown—a static, flat, and stateless storage medium—simply cannot carry the knowledge complexity that comes with a growing codebase.

Three Flaws That Will Cost You Time

1. The Silent Lie: Context Rot

Your .cursorrules file is a one-way street: the agent can read it, but it can almost never write back to it coherently. If you do let the model update the file freely, it quickly dissolves into contradictory chaos.

So, the burden of maintaining “memory” falls squarely on you, the human developer. The pitch sounds great: “It’s just text! I can edit it anywhere, anytime, and update it whenever something changes.”

But ask yourself honestly: in a project that changes daily, when you’ve been refactoring directories, switching state libraries, or wrestling with a bizarre API quirk—how often do you actively pause context, open that Markdown file, and carefully document the lesson you just learned?

The reality? Almost never.

So when you rename app/api/ to app/routers/, the old rules don’t show a compiler error. They don’t give a linter warning. The file just quietly lies to the agent until the AI suddenly suggests a code pattern you abandoned two weeks ago, and you realize you’ve been debugging obsolete advice.

2. Full-File Loading Wastes “Attention”

Every single conversation loads the entire rules file—ask about CSS formatting, and the agent still has to read your database migration rules. Anthropic’s context engineering guide calls this the “attention budget” problem: every irrelevant token in the window degrades processing quality for the relevant ones.

Because there’s no way to load rules on demand, the file becomes less reliable as it grows. Anthropic’s own documentation explicitly states that CLAUDE.md has a practical limit of around 200 lines—beyond that, model compliance with rules drops significantly.

Some developers have even resorted to putting “very-important” in the filename, hoping to trigger the model’s internal attention weight allocation. This is just a band-aid for a structural problem.

3. Long Sessions Compress Memory

This is an architectural flaw of the context window. In long, deep-diving conversations, agents compress early context to make room. One developer running a six-agent production system documented the phenomenon: “Agents silently lose CLAUDE.md directives, forget which files were changed, and redo work from 30 minutes ago. They never tell us.” Writing better rules can’t fix this—it’s a physical limitation of the model’s memory management.

Different Agents, Different Pain Points

Coding Agents (Cursor / Claude Code / Kiro)

Your codebase changes every day. Your rules say “use Zustand,” but you’ve already started introducing Jotai in some components. You update the file, but you miss the old reference on line 47, and the agent starts non-deterministically switching between the two, leaving you to pick up the pieces.

Both Anthropic and GitHub recognized this and offered different solutions. Anthropic added Auto Memory to Claude Code—the agent writes its own notes on build commands, debugging insights, and patterns. GitHub’s Copilot Memory goes further: memories are validated before use—checking whether the referenced code still exists—and unvalidated memories are automatically expired after 28 days.

Both chose to go beyond static files. That says something.

OpenClaw / Browser Automation Agents

OpenClaw stores conversation history in Markdown organized by time period, loading everything at session start, with an upper limit of ~150,000 characters. By the tenth session, most of your context budget is consumed by old, irrelevant chatter.

This spawned an entire ecosystem of replacements: vector-indexed memsearch by Milvus, OpenClaw-specific Mem0 integrations, and MemOS plugins. When multiple companies compete to replace a tool’s primary memory system, the default clearly isn’t working.

The deeper issue? Browser agents need typed relationships—multi-step workflow progress, cross-site data, navigation patterns—and flat text simply cannot express those structures.

Security: The Hidden Vulnerability

Markdown-based agent files aren’t just unreliable—they are a security risk.

  1. MemoryGraft Attack: Malicious agents use your README files as injection vectors, planting fake “successful experiences” that other agents invoke later.
  2. Rules File Backdoor: Invisible Unicode characters are embedded in .cursorrules, redirecting AI code generation to introduce vulnerabilities.

These poisoned rules spread through sharing communities—the “awesome-cursorrules” list alone has 33,000+ stars. OWASP’s 2026 Agentic Top 10 lists memory and context poisoning as a top-tier threat. Every mitigation—provenance tracking, trust scoring, integrity snapshots—requires structured memory. Plain text files cannot implement any of them.

What Production-Grade Agent Memory Should Look Like

Stepping back from specific tools, what must ideal agent memory do? Six requirements emerge:

1. Both humans and agents can write. You set guardrails (static rules); the agent accumulates knowledge on the job (dynamic memory). Two write paths, one shared store.

2. On-demand retrieval, not full-file loading. Retrieve only the few memories most relevant to the current task using semantic similarity. The rest stays out of the context window, improving answer quality and reducing costs.

3. Typed memories with different lifecycles. User preferences (e.g., “use tabs”) should persist indefinitely. Working memory (“currently debugging the auth module”) should expire when the task ends. Project decisions should persist but be easily overridable by newer decisions. Flat files can’t manage this complexity.

4. Contradiction detection and autonomy. If the agent stores “we use PostgreSQL,” then later encounters “tests use SQLite,” a real memory system recognizes this tension: same topic, different conclusions—and either resolves it (different context: production vs. testing) or flags it for the developer’s decision. Markdown files just store both and hope the model guesses correctly.

5. Git-level version control and rollback. Every memory change is recorded. You should be able to snapshot before a major refactor, branch memory for an architecture experiment, or roll back if memory becomes poisoned. This isn’t a nice-to-have—it’s the only reliable defense against memory poisoning.

6. Cross-agent sharing with provenance tracking. Cursor, Claude Code, Kiro, OpenClaw—all should read from and write to the same memory pool. But you need to know which agent wrote what and when to enable auditing and selective trust.

How Memoria Addresses These Requirements

Memoria is an open-source MCP Server—any agent that supports the MCP protocol (Cursor, Claude Code, Kiro, OpenClaw) can connect directly without custom integration. Its architecture maps one-to-one to the six requirements:

Both humans and agents can write. Memoria exposes tools like memory_store, memory_retrieve, memory_correct, and memory_purge via MCP, which agents call automatically. You continue writing static rules in .cursorrules or CLAUDE.md; agents write dynamic knowledge through Memoria. Two layers, each with its own job.

On-demand retrieval. Memoria uses hybrid search—vector similarity plus full-text retrieval—against a MatrixOne database. At the start of a conversation, steering rules instruct the agent to call memory_retrieve, pulling only relevant memories. Everything else stays out of the context window.

Typed memories and lifecycle management. Memoria distinguishes memory types: profile (long-term preferences), working memory (task-scoped, cleaned up via memory_purge at session end), and goal-tracking memory. A session-lifecycle steering rule defines the protocol: retrieve relevant context + active goals at start, accumulate knowledge mid-session, clean up temporary memories at end.

Contradiction detection and autonomy. The memory-hygiene steering rule activates proactive governance. When a new memory contradicts an old one, the system detects the conflict—either resolving it (different context) or quarantining the low-confidence memory. The memory_correct tool is purpose-built for this: instead of blindly appending new facts, it updates existing memories in place.

Git-level version control. This is Memoria’s core differentiator. MatrixOne’s native Copy-on-Write engine provides zero-copy branching, instant snapshots, and point-in-time rollback at the database layer—not application-level patches. Every memory change generates a snapshot with a full provenance chain. You can:

  • Snapshot: Archive current memory before major changes.
  • Branch: Experiment with different approaches in an isolated environment.
  • Rollback: Restore to a known-good state when memory is poisoned.
  • Diff: Compare two snapshots to see exactly what changed.
  • Merge: Bring a successful experiment back into the main line.

This is the same mental model as Git, applied to agent memory. For developers, the learning curve is near zero—you’ve already internalized the metaphor.

Cross-agent sharing. Memoria runs as a standalone MCP Server backed by a database, not as a file embedded in a single tool. All agents connected to the same Memoria instance share one memory pool. Cursor learns you switched to ruff, and Claude Code knows too. An audit trail records every memory written by every agent, keeping provenance clear at all times.

A Pragmatic Migration Path


You don’t need to throw away .cursorrules today. The right approach is to layer:

Keep static rules in Markdown. Coding standards, architectural principles, style guides—things that change on a quarterly cadence. These are your guardrails.

Hand dynamic knowledge to Memoria. Project decisions, lessons learned, workflow state, hard-won debugging insights—things that change every session.

Connect all your agents to the same Memoria instance. Static rules as guardrails, dynamic memory as knowledge, version control as a safety net. That’s the complete architecture.

Memoria—open source, Apache 2.0. Supports cloud hosting and one-click deployment. Give your Cursor / Claude Code / OpenClaw cross-session memory with Git-style undo.

Experience the power of persistent memory for AI Agents. 🧠
💻 GitHub (Star us!):[https://github.com/matrixorigin/Memoria]
🌐 Website: [https://thememoria.ai/]
👾 Discord: [https://discord.com/]

Why Your Contact Form Is Getting Spam (And How to Stop It)

You built a contact form, published it on your website, and started waiting for enquiries. Then one day, your inbox delivers something unexpected: a wall of foreign text, a string of suspicious links, and a name that does not match any real person you have ever heard of.

This is contact form spam, and if you have a public-facing form on the internet, it is only a matter of time before it finds you.

In this post, we will explain exactly what contact form spam is, how bots find your form in the first place, what the real-world consequences of ignoring it are, and most importantly, how to stop it completely using Formgrid’s built-in security tools.

What Is Contact Form Spam?

Contact form spam is the practice of using automated software, commonly referred to as bots, to submit unsolicited and irrelevant content through web-based contact forms. Unlike email spam, which arrives directly in your inbox from an external mail server, contact form spam is generated by a bot that visits your website, locates your form, fills it out programmatically, and submits it just like a real user would.

The content of these submissions varies widely. Some are promotional, some are gibberish, and some are loaded with links pointing to external websites. What they all have in common is that they are not genuine enquiries from real people. There is no human on the other end waiting for your reply.

Here is a real example of a contact form spam submission received by a contact form:

Name: DannyNef

Email: [email protected]

Message: Автоматы газированной воды серии «АТЛАНТИКА» АП-60, АП-100 https://vendavtomat.ru/… Автоматы газированной воды «АТЛАНТИКА»: А-150 Эконом https://vendavtomat.ru/… Редуктор углекислотный https://vendavtomat.ru/…

At first glance, this might look confusing or even alarming. But once you understand what is happening, it becomes much easier to deal with.

Let us break it down:

The name “DannyNef” is not a real person. It is a randomly generated or recycled name used by the bot to fill in the required name field. It bears no relation to the actual operator behind the spam campaign.

The email “[email protected]“ is a throwaway address. The .ru domain indicates it is a Russian email service, and the random string of characters before the @ symbol is a strong indicator that this address was generated automatically. No real business or individual would use an address like this for professional correspondence.

The message itself is a block of Russian promotional text interspersed with URLs pointing to a vending machine supplier website. The text talks about carbonated water machines, product dimensions, and the history of the company. None of it is directed at you or your business. It was not written for you. It was assembled by a bot and blasted across thousands of contact forms automatically.

The phone number “88254351266” follows a Russian phone number format and is almost certainly fake or belongs to an unrelated party.

This type of submission is entirely harmless in terms of your website’s security. The bot has not hacked you, stolen your data, or compromised your system in any way. But it is a sign that your form has no protection in place, and that will have consequences over time.

How Do Bots Find Your Form in the First Place?

This is one of the most common questions people ask when they receive their first spam submission. You may not have shared your form widely. Your website may be relatively new. So how did a bot find it?

The answer is that bots do not need to be directed to your website specifically. They operate at scale across the entire internet, and they use several methods to discover unprotected forms automatically.

Search engine indexing:

Search engines like Google and Bing constantly crawl the web, following links from page to page and indexing content. When your website appears in search results, even for very obscure or low-traffic queries, it becomes discoverable. Bots can use the same public search infrastructure to find pages that contain contact forms, often by searching for common form-related HTML elements or keywords.

Web crawling:

Many spam bots run their own crawlers that systematically visit websites and scan the page source code for form elements. They do not need to read your content or understand what your business does. They are simply looking for form fields and a submit button. Once found, the form is added to a queue and submitted automatically.

Link harvesting:

If your website is linked to from another website, a directory, a social media profile, or any other publicly accessible page, a bot that crawls those sources can follow the link to your site and discover your form.

Data brokers and leaked lists:

Some spammers purchase or obtain lists of URLs known to contain contact forms. These lists are compiled over time and shared or sold within spam networks. Once your URL ends up on one of these lists, it can be repeatedly submitted to by multiple different bots.

Form endpoint scanning:

More sophisticated bots do not even visit your website visually. They scan for exposed form submission endpoints directly and submit data to them programmatically, bypassing your frontend entirely. This is why domain restrictions, which we will cover later, are an important layer of protection.

The key takeaway is that no website is too small, too new, or too obscure to be targeted. Bots are not selective. They are automated, indiscriminate, and operating at a scale that means your form will eventually be discovered, regardless of how little traffic your website receives.

What Happens If You Ignore Contact Form Spam?

Receiving one or two spam submissions a month might feel like a minor annoyance, and in isolation, it is. But ignoring contact form spam entirely and taking no steps to protect your forms can lead to a range of problems that compound over time.

Inbox clutter and missed enquiries:

The most immediate consequence is practical. As spam volume increases, your inbox becomes harder to manage. Real enquiries from genuine customers or clients get buried among spam submissions. In a busy period, it is entirely possible to overlook a legitimate message because it was sandwiched between bot submissions. For a small business, a single missed enquiry could represent a lost client or a missed opportunity.

Wasted time:

Even if you never miss a real message, reviewing and deleting spam submissions takes time. If you receive dozens per week, that time adds up. It is time that could be spent on actual work.

Skewed analytics and data:

If you use your form submissions to inform decisions, for example, tracking how many enquiries you receive per week or what topics people are asking about, spam submissions will distort that data. Your numbers will be inflated with junk, making it harder to draw accurate conclusions.

Reputational risk from scraped email addresses:

When a bot submits your form, it often also scrapes any publicly visible email addresses on the same page. If your contact email is visible on your website, it may be harvested and added to spam lists, leading to an increase in email spam in your inbox as well.

Server load and rate abuse:

High-volume bot attacks can submit your form hundreds or thousands of times in a short period. Depending on your hosting setup, this can put unnecessary load on your server, slow down your website, or, in extreme cases, trigger resource limits that affect your site’s availability.

Data pollution in your CRM or connected tools:

If your Formgrid forms are connected to a CRM, email marketing tool, or internal database via integrations or webhooks, spam submissions may be passed through to those systems as well. This can pollute your contact lists with fake entries, trigger automated email sequences to non-existent addresses, and harm your email sender reputation over time.

None of these outcomes is inevitable if you act early. The good news is that Formgrid gives you everything you need to protect your forms before any of these problems take hold.

How to Protect Your Formgrid Forms: A Step-by-Step Guide

Formgrid.dev includes a dedicated Security Settings section for every form, available on all plans, including the free plan. Enabling protection takes less than two minutes and requires no technical knowledge.

Here is exactly how to do it.

Step 1: Log In to Your Formgrid Account

Navigate to Formgrid.dev and log in with your credentials. You will land on your dashboard, where all your forms are listed.

Step 2: Open the Form You Want to Protect

Click on the form you want to secure. This will take you to the Form Details page, where you can manage everything related to that specific form.

Step 3: Navigate to the Settings Tab

At the top of the Form Details page, you will see a row of tabs. Click on the Settings tab to open the form’s configuration options.

Step 4: Scroll Down to the Security Settings Section

Within the Settings tab, scroll down until you reach the Security Settings section. This is where all of Formgrid’s spam and abuse protection tools are located.

You will see the following options:

Enable CAPTCHA:

Adds a Google reCAPTCHA “I’m not a robot” verification step to your form. When enabled, anyone submitting your form will need to complete the CAPTCHA challenge before their submission goes through. Real human users can do this easily. Automated bots typically cannot, which stops the majority of spam submissions before they ever reach your inbox.

Enable Honeypot:

Adds a hidden field to your form that is completely invisible to real users but visible to bots. When a bot fills in the form, it fills in every field it can detect, including the hidden honeypot field. Formgrid detects this and automatically discards the submission as spam. Legitimate users never see or interact with this field, so it has no impact on the experience of real visitors filling in your form.

Allowed Domains (CORS):

Allows you to specify which domains are permitted to submit data to your form. If your form is embedded on a specific website, you can enter that domain here. Any submission coming from a different domain will be rejected. This is particularly effective against bots that submit directly to your form endpoint without visiting your website at all.

Rate Limiting:

Sets a maximum number of submissions allowed per IP address per minute. This prevents any single source from flooding your form with repeated submissions in a short period. If you expect normal users to submit your form occasionally, a limit of 3 to 5 submissions per minute per IP is a reasonable starting point.

Step 5: Enable Your Preferred Protections and Save

Toggle on the settings you want to activate. For most public contact forms, enabling both CAPTCHA and Honeypot together provides strong protection against the vast majority of spam bots.

Once you have made your selections, click Save to apply the changes.

Your form is now protected.

Which Security Settings Should You Use?

You do not have to choose just one option. The settings are designed to work together, and combining them gives you layered protection that is significantly harder for bots to bypass.

Here is a practical guide based on your situation:

For a standard public contact form:

Enable both CAPTCHA and Honeypot. This combination stops bots at two different points in the submission process and covers both sophisticated and unsophisticated bot behaviour.

For a high-traffic form receiving frequent spam:

Enable CAPTCHA, Honeypot, and Rate Limiting together. Set your rate limit conservatively. If a single user should never need to submit your form more than once or twice per minute, set the limit accordingly.

For a form embedded on a single known website:

Add your domain to the Allowed Domains field in addition to enabling CAPTCHA and Honeypot. This prevents bots from submitting directly to your form endpoint, bypassing your website’s frontend entirely.

For a form used internally or by a known audience:

If your form is not intended for the general public, consider using Allowed Domains to whitelist only the specific domains that should have access. This adds a strong access control layer on top of the spam protection tools.

A Summary of Security Best Practices

Formgrid’s Security Settings section reflects industry-standard guidance for protecting web forms. Here is a summary of the recommended approach:

Enable CAPTCHA for all public-facing forms:

Any form that can be accessed and submitted by anyone on the internet should have CAPTCHA enabled. This is the single most impactful step you can take to reduce spam volume immediately.

Set rate limits appropriate to your expected traffic:

Think about how often a real user would realistically need to submit your form. Set your rate limit to reflect that. A rate limit that is too high offers little protection; one that is too low may frustrate legitimate users.

Configure CORS to restrict access to your domains:

If you know exactly where your form will be used, lock it down to those domains. This eliminates an entire category of bot attacks that never even visit your website.

Use the Honeypot as a passive, frictionless layer:

Unlike CAPTCHA, the Honeypot requires nothing from your users. It works silently in the background and catches bots that might find ways around other protections. There is no reason not to have it enabled at all times.

Final Thoughts

Contact form spam is a universal experience for anyone running a public-facing website. It is automated, indiscriminate, and growing in volume as bots become more sophisticated. But it is also entirely preventable with the right tools in place.

Formgrid gives every user, including those on the free plan, access to a full suite of security settings designed specifically to address this problem. Enabling CAPTCHA and the Honeypot takes under two minutes and will stop the overwhelming majority of spam submissions before they ever reach your inbox.

Do not wait until your inbox is overwhelmed to take action. Open your form settings today, enable your protections, and get back to focusing on the messages that actually matter.

If you have any questions about configuring security settings on your Formgrid forms, the support team is always available to help.

Friday Shipped & Scarred #1: The Day git –force Deleted 130 PC Workman Commits (I Got 90 Back)

Friday Shipped & Scarred #1: The Day git –force Deleted 130 Commits (And How I Got 90 Back)

First in a weekly series tracking PC_Workman development. The good, the broken, the lessons learned.

Monday: My Heart Stopped

I ran git push --force on PC_Workman.
130 commits became 1 commit.

Eight months of development history disappeared.
Not the code. The code was fine. But the story was gone.

Every “Fixed memory leak” commit. Every “Rebuilt UI (again)” message. Every timestamp proving this project wasn’t slapped together over a weekend.
Gone.

The Panic

First reaction: freeze.
Second reaction: check GitHub.

One commit. Master branch. That’s it.
Reflog? Empty.

Old branches? None with the history.

GitHub API endpoints? Nothing recoverable.

I’d read about this happening to others. “Always backup before git surgery” they said.
I didn’t backup this time…

The Recovery: An Accidental Save

Then I remembered something.

Three weeks ago, I created an archive branch.

Not for backup. Just to keep old files around for reference. Old UI screenshots. Deprecated features. That kind of thing.

I checked it.

90 commits.

Not the full 130. But 90 commits with complete history dating back months.

The archive had saved me.

What I Lost (And What I Kept)

Lost:

  • 40 commits (most recent work)
  • Detailed messages about recent bugfixes
  • Exact timeline of last month’s changes

Kept:

  • 90 commits showing project evolution
  • Proof of 8 months of development
  • Complete rebuild history (I did 4 complete UI rebuilds – that’s documented)

Could have lost:

  • Everything.

90 out of 130 = 69% recovery rate.

Not perfect. But infinitely better than 1 out of 130.

This Week: Cleaning Up What The Disaster Exposed

The git crisis had a silver lining: it forced me to look closely at the repository.

And I found… problems.

Problem 1: Missing CONTRIBUTING.md

Someone opened an issue: “CONTRIBUTING.md link is broken”
I checked. The file was gone.
Lost in the git chaos. Never recovered from the archive.

Solution: Rewrote it from scratch. Better this time. Clearer guidelines. Actual examples.
Result: Contributor can now actually contribute.

Problem 2: AI-Generated Patterns in README

Last week I posted on Reddit asking why my project looked “AI-generated.”
Got brutal, honest feedback:

  • Empty bullet points (- with no text)
  • Emoji bullets (everywhere)
  • AI-style phrasing (“Why it’s different” with arrow format)

Solution:

  • Removed all empty bullets
  • Removed emoji formatting
  • Rewrote descriptions in plain English
  • Added actual technical depth from CHANGELOG

Before:

  • Monitors your PC
  • Shows graphs

After:
Real-time CPU, GPU, RAM tracking with historical analysis.
Dashboard loads optimized from 800ms to 200ms through widget reuse pattern.

Specific. Technical. No fluff.

Problem 3: Polish UI Text (In An English Project)

I thought I’d translated everything.
I was wrong.

Found Polish text in:

  • Status messages (“Ładowanie…” = “Loading…”)
  • Dashboard labels (“Użycie CPU” = “CPU Usage”)
  • Error messages
  • Placeholder tooltips

Files changed:

  • ui/windows/main_window.py
  • ui/windows/main_window_expanded.py
  • ui/components/fan_dashboard.py
  • ui/components/sidebar_nav.py
  • ui/pages/fan_control/usage_stats.py
  • ui/pages/fan_control/hardware_info.py
  • 6 more files

Time spent: 6 hours
Times I thought I was done: 3
Times I actually was done: 1

Problem 4: Marketing Docstrings

Early code had docstrings like:

def update_dashboard():
    """
    Revolutionary real-time dashboard update system.
    Leverages advanced widget reuse patterns for optimal performance.
    Seamlessly integrates with monitoring engine.
    """
    # ... actual code

Nobody talks like this in real code.

After cleanup:

def update_dashboard():
    """Update dashboard widgets. Reuses existing widgets instead of recreating."""
    # ... actual code

Boring. Accurate. Better.

Removed from:

  • Main window files
  • Fan dashboard
  • All page components

Result: Code reads like code, not a sales pitch.

Problem 5: Guide-AI Repository (Side Project)

While cleaning PC_Workman, noticed Guide-AI repo was stale:

  • No screenshots (people can’t see what it does)
  • No tags
  • Outdated description

Fixed:

  • Added QR code demo screenshots
  • Added proper GitHub topics (react, supabase, ai, qr-codes)
  • Rewrote description with actual value prop
    Not perfect. But visible now.

What Shipped This Week

Repository cleanup:

  • CONTRIBUTING.md rewritten
  • README cleanup
  • 12 files Polish -> English translation
  • Marketing docstrings -> technical docstrings
  • Guide-AI repo updated

Stats:

  • 90+ commits recovered and visible
  • 0 empty bullet points remaining
  • 0 Polish UI strings remaining
  • 0 “leverages advanced” phrases remaining

What Broke

Git recovery wasn’t perfect:

  • Lost 40 commits
  • Lost detailed timeline of February changes
  • Had to manually recreate some documentation

Translation took 3x longer than expected:

  • Found Polish text in 12 files (thought it was 4)
  • Each pass found more hidden strings
  • Final check: found 3 more files

Time management:

  • Planned: 2 hours for cleanup
  • Actual: 8 hours across 3 days

Lessons Learned

1. Before ANY risky git operation:

git branch backup-$(date +%Y%m%d)

5 seconds. Could save months.

2. “Clean” code has layers

First pass: obvious issues

Second pass: hidden patterns

Third pass: still finding things

Clean once isn’t enough.

3. Accidental backups count as backups

That archive branch saved me.
Wasn’t intentional. Still worked.
Lesson: Keep old branches longer than you think necessary.

4. Reddit feedback hurts but helps

Being told your project “looks AI-generated” stings.
But it pushed me to actually clean up the issues.
Feedback <3.

Next Week:

-Monday Blueprint Grind #1
-Wednesday Code Autopsy #2

Topic: Process aggregation – how PC_Workman tracks per-process CPU/RAM usage without destroying performance.

Format: Technical deep-dive into hck_stats_engine/process_aggregator.py

Also coming:

  • First YouTube Short (Code Autopsy video format)
  • v1.7.0 planning (TURBO mode research)

The Numbers

PC_Workman current state:

  • Version: 1.6.8
  • Commits visible: 90+ (recovered)
  • Development time: 800+ hours
  • Next milestone: v2.0 → Microsoft Store (Q3 2026)

This week’s time investment:

  • Git recovery: 2 hours
  • README cleanup: 1 hour
  • Code translation: 6 hours
  • Documentation: 1 hour

Total: 10 hours (planned 4, actual 10)

Try It / Break It / Fix It

PC_Workman is open source. MIT licensed.

GitHub: github.com/HuckleR2003/PC_Workman_HCK

Download: Latest release

Follow the journey:

Building in public. Shipped & Scarred every Friday.

Questions, comments, roasts? Hit me up.

About the Author

I’m Marcin Firmuga. Solo developer and founder of HCK_Labs.

I created PC Workman , an open-source, AI-powered
PC resource monitor
built entirely from scratch on dying hardware during warehouse
shifts in the Netherlands.
This is the first time I’ve given one of my projects a real, dedicated home.

Before this: game translations, PC technician internships, warehouse operations in multiple countries, and countless failed projects I never finished.

But this one? This one stuck.
800+ hours of code. 4 complete UI rebuilds. 16,000 lines deleted.
3 AM all-nighters. Energy drinks and toast.

And finally, an app I wouldn’t close in 5 seconds.
That’s the difference between building and shipping.

PC_Workman is the result.



GitHub Copilot Stores Derived State in useEffect. This Is Why That Breaks Your App.

There is a pattern that shows up in almost every React project where AI was involved.

It looks harmless. It works on first glance. And it silently creates bugs that are annoying to track down.

It looks like this:

const [total, setTotal] = useState(0)

useEffect(() => {
  setTotal(items.reduce((sum, i) => sum + i.price, 0))
}, [items])

GitHub Copilot writes this confidently. No warnings. No errors. Just broken logic waiting to happen.

What is actually wrong here

Total is not state. Total is a calculation.

It depends entirely on items. Every time items changes, total gets recalculated anyway. Storing it in useState and syncing it with useEffect adds a second source of truth that never needed to exist.

The result is a component that renders twice when it should render once. State that is always one step behind. And a useEffect that exists for no reason other than that the AI had no rule against it.

Why GitHub Copilot does this

Copilot is not broken. It is pattern matching.

It has seen thousands of examples where useState and useEffect are used together. So it reaches for that pattern by default even when the situation does not call for it.

It has no concept of derived state. It does not ask whether something should be stored or computed. It just generates what looks plausible.

And plausible is not the same as correct.

The fix is one line

const total = items.reduce((sum, i) => sum + i.price, 0)

No useState. No useEffect. Just a variable that is computed directly from items on every render.

One render instead of two. One source of truth instead of two. Zero unnecessary state.

The deeper problem

useEffect misuse is one of the most common AI coding mistakes in React projects.

Not because the pattern is rare. Because Copilot reaches for it constantly for derived state, for synchronous logic, for things that should never be effects in the first place.

Every area where your AI has no explicit constraints is an area where it improvises. And improvised useEffect logic compounds fast in a real React codebase.

Want to find where your React project has these problems?

I built a free 20 point checklist that helps you identify exactly this. Structural weaknesses that make AI output unpredictable and your app fragile.

No guessing. No endless follow up prompts. Just a clear picture of what to fix before you prompt again.

👉 Get the React AI Debug Checklist free

And if you want the full system, rules across architecture, typing, state, accessibility, and more:

👉 Avery Code React AI Engineering System

Same AI. Different rules.

© Avery Labs — Avery Code

90% of Local Businesses Are Invisible Online — And Their Reviews Prove It

I scanned 100 businesses across Lahore, Saudi Arabia, and Japan. What I found wasn’t a gap — it was a gulf. High ratings, loyal customers, zero digital presence. Here’s the exact data.

Tags: ai startup webdev productivity

The Problem I Wasn’t Expecting

I started this research to find businesses that needed digital help. I expected maybe 40–50% to have gaps. What I found was closer to 90%. Not 90% with “room for improvement” — 90% with nothing. A Google Maps pin. Sometimes a phone number. That’s it.

The assumption I had going in — that weak online presence correlates with weak businesses — was completely wrong. Some of the best-reviewed businesses I found had the worst digital footprints.

“A car wash in Japan with 4.8 stars and 200+ reviews. No website. No booking system. No social presence. People loved the place — Google just couldn’t tell anyone about it properly.”

That single example reframed the entire opportunity for me. This isn’t about bad businesses. This is about good businesses being invisible to customers actively searching for them right now.

What the Data Actually Shows

I pulled 100 local businesses across three markets: restaurants in Lahore, service businesses in Saudi Arabia, and specialty services in Japan. I was checking each one for five signals: website existence, website traffic/activity, social media presence, SEO indexing, and Google Business completeness.

Signal Result
Had a functioning, trafficked website ~10 businesses
Had a website but zero visible traffic or SEO ~15 businesses
Had only a Google Maps pin — nothing else ~75 businesses
Had active social media (Instagram/Facebook) < 12 businesses
Had 4.0+ star rating despite weak presence ~60 businesses

That last row is the number that matters. 60 businesses with strong social proof and near-zero digital reach. That’s not a market problem. That’s a distribution problem — and distribution problems are solvable with software.

Three Cases That Made This Real

🍜 Restaurant in Lahore, Pakistan

  • Rating: 4.6 stars · Reviews: 180+ · Website: None

The place had a line on weekends according to the reviews. People drove across the city for it. When I searched for their cuisine category in their area — they didn’t show up in the top 10. They existed only to people who already knew them.

What they’re losing: Every tourist, every food blogger, every new resident who searches Google first — which is everyone.

🚗 Car Wash in Japan

  • Rating: 4.8 stars · Reviews: 200+ · Website: None · Bookings: Walk-in only

This one surprised me most. Japan has extremely high digital adoption. Yet this business — clearly exceptional at what they do — had no way for a customer to book ahead, check wait times, or even confirm hours. Their reviews mentioned the wait was “worth it.” Imagine if people could schedule.

What they’re losing: Corporate fleet contracts, advance bookings, customers who won’t wait in uncertainty.

🏪 Service Business in Saudi Arabia

  • Rating: 4.3 stars · Website: Exists — but dead. No traffic, last updated 2021, loads in 8+ seconds on mobile, contact form returns a 404.

This was the “has a website” category — which sounds better until you look closer. Having a bad website is sometimes worse than having none, because it creates false confidence.

What they’re losing: Any customer who clicks through and bounces — which is every customer.

Why This Happens — It’s Not What You Think

The obvious assumption is that these business owners don’t care or don’t know. That’s not what the data suggests. Most of them have Google Business profiles — which means they claimed their listing, took the step, and stopped there.

The gap isn’t awareness. It’s execution cost.

Building a website, doing SEO, managing social — each one is a part-time job. For a restaurant owner running a kitchen 14 hours a day, it’s genuinely not possible. The market failure here isn’t knowledge, it’s access to affordable, intelligent tooling that does this work without requiring them to become digital marketers.

“The barrier isn’t that they don’t want to be found. It’s that being findable costs more time than they have.”

What I’m Building — And What It Won’t Do

I’m building an agentic system that does three things:

  1. Finds these businesses automatically using real-time data from Google Maps and search APIs
  2. Audits their complete digital presence across every platform
  3. Generates a detailed report on exactly what’s missing and what it’s costing them

I want to be honest about where it stands.

What it does well now:

  • Identifies no-website businesses at scale
  • Pulls live Google Maps data via SerpAPI
  • Cross-references review quality vs. digital presence
  • Generates structured audit reports using Gemini API
  • Works across multiple countries and languages

What it doesn’t do yet:

  • Verify if a business is still active vs. permanently closed
  • Assess in-store foot traffic or revenue signals
  • Distinguish seasonal inactivity from abandonment
  • Work reliably in markets with low Google Maps coverage
  • Automate outreach to identified leads

The limitations matter. A 4.5-star restaurant in Lahore that hasn’t updated its Google listing in 6 months might have closed — or might just not care about Google. The system can’t distinguish those yet. That’s phase 3 work.

The Market Size Nobody Is Talking About

Google Maps has over 200 million business listings. Estimates on businesses with no functioning website range from 36% to 64% depending on the market. In South Asia and MENA — where my initial scan focused — the number skews toward the high end.

More importantly: these businesses are not digitally hopeless. They’re digitally underserved. They have products people want, reviews proving it, and zero infrastructure to scale what’s working. That’s an optimization problem — which is exactly what AI agents are suited to solve.

The question I keep coming back to: if 90 out of 100 businesses I looked at manually have this problem, and the manual process took me hours — what does this look like when the scan runs automatically across 10,000 businesses a day?

What I’d Test Next

If I were to run this scan again with more resources, I’d focus on three things:

  1. Expand data sources — add Yelp and TripAdvisor alongside Google Maps to triangulate presence gaps more accurately
  2. Add a website performance layer — load time, mobile score, last updated date — rather than just checking existence
  3. Track review velocity, not just rating — a business getting 5 new reviews a week is a very different lead than one with 200 reviews and nothing in 18 months

I’d also want to run this against a control group of businesses that did invest in digital presence and measure the review growth delta. That comparison would sharpen the ROI story considerably.

The Opportunity Is Real. The Data Says So.

I scanned 100 businesses. 90 of them were effectively invisible to customers who weren’t already loyal. Most of them had the product quality to deserve more customers — they just had no mechanism to find them.

That’s not a niche problem. That’s the default state of local business in 2025.

An agentic system that identifies, audits, and surfaces these gaps — at scale, in real time, across markets — is addressing something the current tooling ecosystem completely ignores.

The scan was manual. The next one won’t be.

Building the agent that makes this automatic — follow for updates on the build.

5 Ways Developers Use Screenshot APIs (Beyond Simple Page Captures)

When people hear “screenshot API,” most of them picture a pretty straightforward task: send a URL, get back an image of the page. And that is the basic scenario, sure. But developers who’ve already integrated a screenshot API into their projects tend to use it for things you might not have considered at all.

In this article, I’ve put together five real scenarios where a screenshot API can save you hours of work and solve problems that are either difficult or expensive to handle any other way.

1. Automatic OG Image Generation

Open Graph images are those previews that pop up when you share a link on Slack, Twitter, LinkedIn, or WhatsApp. If a page doesn’t have an OG image, the shared link looks bland and gets fewer clicks. I actually covered how to create Open Graph images both manually and automatically in a previous article, so feel free to check that out for more detail.

As I mentioned there, the core problem is that every page needs its own image. If you have a blog with 50 posts, that’s 50 images you need to create and maintain. And if you’re running a SaaS product with dynamic pages — user profiles, dashboards, reports — then creating OG images by hand isn’t realistic at all.

In cases like these, a screenshot API makes the whole thing much simpler, and the workflow is very sequential: you build an HTML template with the design you want (title, logo, background), render it through the API, and get back a ready-made image at 1200×630 — exactly the size social networks recommend. The entire process is automatic, with images generated on the fly or on a schedule. I won’t go into too much detail here since I covered the full breakdown in that earlier post.

2. Link Previews for Directories and Catalogs

If you’re building a site directory, a marketplace, an aggregator, or really any product where users submit links to external resources, you need visual previews of those links. Without them, your catalog ends up looking like a boring list of URLs.

A screenshot API lets you automatically generate a thumbnail for every site that gets added. In practice, it works like this: a user pastes a URL, your backend fires off a request to the API, gets back an image, and displays it as a card. No manual work involved, and no need to ask users to upload screenshots on their own.

This approach is used by site directories, aggregators like Product Hunt, bookmarking tools, and even internal corporate portals where employees share useful resources. Now, you might say that AI image generators can quickly produce any image you need these days, but this is a different thing entirely. With a screenshot API, you send dynamic HTML and get back a preview image in the exact style and design you prepared — no hallucinations, no randomness, just precisely what you asked for. On top of that, it’s significantly faster than working with an AI model to generate images one by one.

3. Visual Regression Testing During Deploys

This is probably one of the most practical scenarios for developers. The idea is simple: before each deploy, you take screenshots of your key pages, then take them again after the deploy and compare. If something broke visually, you catch it before your users do.

Without a screenshot API, this process requires running a headless browser on your CI/CD server, which adds a fair amount of complexity: you need to install Chromium, manage memory, handle timeouts. I actually wrote about this whole process in detail in my article on how to take a website screenshot with Node.js. A screenshot API strips away all that infrastructure — you just send an HTTP request and get back an image.

A typical pipeline looks something like this: your CI/CD triggers a script that captures screenshots of 10 to 20 key pages through the API, compares them pixel by pixel against the previous versions, and sends an alert to Slack if the difference exceeds a set threshold.

4. Archiving and Compliance

In certain industries — finance, legal, pharma — companies are required to keep a record of what their website looked like at a given point in time. This comes up during audits, legal disputes, and regulatory compliance checks.

A screenshot API lets you automatically take daily or weekly snapshots of the pages you need and save them with a timestamp. And then if a dispute ever comes up, you have visual proof of exactly what was published on the site on a specific date.

This same approach is also useful for marketing teams that deal with sponsored content. Advertisers regularly ask for proof that their placement went live, and an automated screenshot with a date and URL handles that without any manual effort on anyone’s part.

5. Competitor Monitoring and Change Tracking

The last scenario is taking regular screenshots of competitor pages to track changes. This could be a pricing page, a homepage, an ad campaign landing page, or even search results for specific keywords.

Developers typically set up a cron job that once a day (or more often) captures screenshots of a list of URLs through the API and stores them. Over time, this builds up a visual history of changes that you can analyze — when a competitor updated their pricing, redesigned their site, or launched a new campaign.

Some go even further and add automated screenshot comparison: if the current snapshot differs from the previous one by more than a certain percentage, the system fires off a notification. That way, you find out about changes on competitor sites the same day, without having to check them manually. I’m actually planning to add this feature to our service in the future as well.

The Common Thread

If you look at all five scenarios, there’s one idea running through them: a screenshot API turns visual information into data you can work with programmatically. Instead of opening a browser, manually taking a screenshot, and saving it somewhere, you send an HTTP request and get back an image that you can process, store, compare, or display to a user — all of it automatically.

And the more pages you need to handle, the more time it saves.

Screenshotrun API lets you capture screenshots of any web page with a single HTTP request. 300 free screenshots per month, Bearer authentication, full-page and viewport modes.